From c70bcbf6b41ae459da34be50bd55e830e7aa9d9e Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Sat, 8 Aug 2026 14:17:21 -0700 Subject: [PATCH 01/29] Direction round: verdict-routed hand, MY PICK card, salience parity, Safer/Bolder registers (#531) * Route the direction hand by verdict, add the pick card, enforce salience parity The decision round previously rendered every dealt challenger as an equal full card whatever the weighing said, so a world that fused poorly (an underwater world dealt to a flower shop) sat at the same visual weight as the assigned direction, and concept-level fusion had no surviving output. Three changes, all presentation-layer; the dice, the assignment, and the two-axis weighing are untouched: - Verdict routing: the weighing closes with wins / competitive / declined per challenger, decided before any borrowing. Declined challengers render demoted (narrow, quiet, catalog art as a labeled thumb, "Adopt anyway"), reordered to the end of the deck by the page itself, still adoptable, never silently dropped. Donations return as named "raised by" lines on the assigned card: a declined challenger donates ambition and system discipline, never its clothes. - The pick card: one card for the model's top-ranked grounded candidate when the dice assigned another, kicker MY PICK, honest familiarity risk on its face. One card, never a ranked list, never the lead position; the anti-menu rule survives with exactly this carve-out. - Salience parity: a card's imagery weight is capped by the assigned card's. With a text-only assigned card (no image generation in the harness), full-bleed catalog heroes demote to labeled thumbs, so what looks important is the verdict's call, never rendering luck. serve-question payload gains additive fields (verdict, kept, raised); old payloads render unchanged. concept-seed's rendered instructions carry the verdict/donation contract and the pick-card carve-out. Covered by two Playwright tests in the new-work e2e suite (verdict routing + parity). Design exploration and rationale were worked through with the maintainer; research grounding is impeccable.style/research lessons 3-5. AI-assisted change. Co-Authored-By: Claude Code * Add Safer/Bolder re-roll registers to the direction round The re-roll gains the user's steering wheel on the familiar-to-bold axis. The decision page renders two register buttons beside the plain re-roll (payload: reroll: { registers: ["safer", "bolder"] }; booleans still work), the answer carries the chosen register, and concept-seed gains --register. The design constraint that shaped the implementation: a register changes only what a round INSTRUCTS, never what it DEALT. The same key and reroll count reproduce the same deal whatever the register, so the exclusion chain never forks and the reproduction contract holds with no API change. - bolder: the dealt foreign forms become the whole hand, every challenger a full card; the first-dealt challenger leads (assignment by deal order, so the dice still choose). The pick card sits out; the canon stays. - safer: the round's dealt hand is spent unseen and stays excluded; the model presents its remaining conventional grounded candidates (at most three) plus the canon executed against named competitors. This is the one sanctioned lineup of the model's own ranked list, existing only by explicit user request. Works degraded (needs no catalog); bolder degrades to a plain grounded round, disclosed. Registers are user steering, never the model's to pre-select. Covered by a concept-seed unit test (same-deal invariant, validation) and a Playwright test (button, answer field, REGISTER directive). AI-assisted change. Co-Authored-By: Claude Code * Add the execution-contract round: comp-led or code-led, chosen after the direction The build previously went comp-led for everyone, silently: a generated comp led and the build chased it, which produces the boldest compositions and also the measured worst-of-both-worlds failure (ambitious design landed poorly, no motion, fix rounds after). Models already defect from it by quietly skipping comp generation, which is unsanctioned code-led with no contract to catch it. This makes the fork explicit and both paths defection-proof: - Comp-led: the comp is law and non-optional once chosen; visualize.md and the comp-is-king build phases run as today. - Code-led: no comp of this page, skipped by contract rather than drift. The QUALITY BAR boards still calibrate finish, and the ambition moves into the written direction contract (FIRST VIEWPORT plus a named signature interaction and motion grammar), audited by the finish reviewer in behavior. Not a discount on commitment. Placement: a second round on the same open table, right after the direction lands. Sketches stay in the direction round (they pick the world); comps are what code-led skips (they bind the composition). The chosen world sets the default lead; the user flips freely; a standing preference recorded in PRODUCT.md skips the round on later surfaces; with no image generation there is no fork, code-led is the only path. Mechanism: serve-question gains payload-level followup: true, which keeps the detached server alive after a pick (exactly like re-roll), swaps the page to the loading hand instead of goodbye, marks the answer with followup: true so --wait keeps the table, and prints a FOLLOWUP OPEN directive telling the agent to deliver the next round via --update. Covered by a Playwright test driving the full two-round flow. AI-assisted change. Co-Authored-By: Claude Code * fix: address PR review bot findings - Degraded safer register no longer contradicts itself (greptile, Copilot, cursor): the degraded template previously said "the assigned index is suspended; the user picks" and then emitted ASSIGNED INDEX, the mandatory build instruction, and the restated footer anyway. The degraded safer path now suppresses the assignment machinery entirely, matching the non-degraded safer round, and restates the user-picks behavior for truncated readers instead. - A declined card's declared sketch no longer renders a full media face (Copilot): the renderer ignores sketch slots on declined cards outright, so a stray sketch cannot buy back the salience the verdict took away. - Bolder rounds no longer carry the generic weighing instruction (cursor): it measures against the assigned grounded direction, which the bolder register suspends; a leader-relative variant weighs the fused challengers against the first-dealt leader instead. All three pinned by new assertions in tests/concept-seed.test.mjs and tests/new-work-e2e.test.mjs. AI-assisted change. Co-Authored-By: Claude Code * fix: followup never arms the loading hand in blocking serve mode cursor[bot] caught a client/server disagreement: the page interpolated its FOLLOWUP constant from the payload alone, so a followup: true payload served in blocking mode (no --start) would leave the browser on a loading hand that nothing resolves, since a blocking server exits on any pick and has no update channel. The page constant is now armed only when the server is detached, blocking rounds get the goodbye screen as before, and new-work.md states that followup belongs only on a detached round; blocking and structured-tool channels run the build-path round as its own second question. Pinned in tests/serve-question.test.mjs. AI-assisted change. Co-Authored-By: Claude Code * Add card-kind choice telemetry and the bolder routing disambiguation The choice ping previously fired only when a dealt catalog challenger won, so pick-share and canon-share had no denominator and the decision page's new spectrum could not be measured. The ping now fires once per resolved attended round on API-dealt rolls: --kind names which card class won (assigned / pick / challenger / canon), --chosen carries the catalog id only when a dealt challenger won, and --register rides along when the round came from a steered hand. Grounded candidates' names never leave the machine (the ping carries the kind alone), the legacy id-only shape stays valid, and DO_NOT_TRACK / IMPECCABLE_NO_TELEMETRY still disable the ping entirely. The seed's TELEMETRY block teaches the new invocation. Also the naming-collision guard: "bolder" said while a direction round is open routes to the Bolder hand register, never the bolder refinement command; one line each in bolder.md and new-work.md. The /api/chosen field additions land in a sister impeccable-site PR; the API ignores unknown fields meanwhile, so this is safe to ship first. AI-assisted change. Co-Authored-By: Claude Code * fix: ping test survives a DO_NOT_TRACK shell cursor[bot]: the pingChosen unit test cleared only IMPECCABLE_NO_TELEMETRY, so a developer shell with DO_NOT_TRACK set failed the success-path assertions. The test now clears both, restores prior values in finally, and passes under DO_NOT_TRACK=1. AI-assisted change. Co-Authored-By: Claude Code --------- Co-authored-by: Claude Code --- skill/reference/bolder.md | 2 + skill/reference/new-work.md | 12 +- skill/reference/visualize.md | 2 +- skill/scripts/concept-seed.mjs | 195 ++++++++++++++++++++++++++----- skill/scripts/serve-question.mjs | 195 +++++++++++++++++++++++++------ tests/concept-seed.test.mjs | 94 ++++++++++++++- tests/new-work-e2e.test.mjs | 168 +++++++++++++++++++++++++- tests/serve-question.test.mjs | 5 + 8 files changed, 598 insertions(+), 75 deletions(-) diff --git a/skill/reference/bolder.md b/skill/reference/bolder.md index ec773a9dd..aff6667b1 100644 --- a/skill/reference/bolder.md +++ b/skill/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/skill/reference/new-work.md b/skill/reference/new-work.md index 20a956129..c39e84810 100644 --- a/skill/reference/new-work.md +++ b/skill/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node {{scripts_path}}/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node {{scripts_path}}/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node {{scripts_path}}/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node {{scripts_path}}/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -82,7 +84,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/skill/reference/visualize.md b/skill/reference/visualize.md index c6402a054..d6e3764d8 100644 --- a/skill/reference/visualize.md +++ b/skill/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. 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. 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. diff --git a/skill/scripts/concept-seed.mjs b/skill/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/skill/scripts/concept-seed.mjs +++ b/skill/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only 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, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${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. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - 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}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${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. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a 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. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; 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) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}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}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/skill/scripts/serve-question.mjs b/skill/scripts/serve-question.mjs index 2d8c24a6a..bbbbdf77e 100644 --- a/skill/scripts/serve-question.mjs +++ b/skill/scripts/serve-question.mjs @@ -29,6 +29,17 @@ * "materials": ["letterpress", "newsprint"], // optional, rendered as tags * "viewport": "one line: the first-viewport composition", // optional * "case": "one line: the fusion verdict, honest", // optional + * "verdict": "competitive", // optional routing tier: "wins" | + * // "competitive" | "declined". Declined cards + * // render demoted after the full cards: + * // narrow, quiet, catalog art as a labeled + * // thumb, "Adopt anyway" instead of "Build + * // this". Still choosable; never deleted. + * "kept": "one line: what the direction kept from this declined world", + * "raised": [ { "from": "challenger-x", "raise": "one line" } ], + * // assigned card only: donations taken from + * // declined challengers, rendered as named + * // raise lines under the identity row * "risk": "one line: the honest risk", // optional * "body": "fallback prose when the structured fields are absent", * "sketch": ".impeccable/sketches/assigned.webp", // optional; may not exist @@ -41,13 +52,25 @@ * }, ... * ], * "reroll": true, // adds a re-roll action (returns {"optionId":"reroll"}) + * // or { "registers": ["safer", "bolder"] } to add + * // the register steers beside it: the answer then + * // carries "register" and the agent re-runs + * // concept-seed with --register * "canon": true, // adds the "Play it straight" standing exit; * // direction rounds only (returns {"optionId":"canon"}) * "canonCard": { ... }, // optional: the standing exit as a full card with the * // same anatomy (label, thesis, palette, sketch, ...); * // rendered last and visually subordinate. Without it, * // canon stays a quiet footer action. - * "steer": true // adds a free-text steer field returned with any answer + * "steer": true, // adds a free-text steer field returned with any answer + * "followup": true // this round's pick is not terminal: the server + * // stays open awaiting --update with the next + * // round (detached mode only), the page shows a + * // loading hand instead of goodbye, and the + * // answer carries followup:true so --wait knows + * // to keep the table. Use it when a decision has + * // a known second half, e.g. direction first, + * // then the execution contract. * } * * Options render as large cards: the sketch leads when present, with the @@ -129,6 +152,12 @@ function printAnswer(raw) { if (a.optionId === 'canon') { console.log('CANON CHOSEN: the user picked the category standard on purpose. Ask once for two or three products this should sit alongside; their craft level becomes the quality bar. Execute the canon at full commitment, conventions embraced without irony or smuggled quirk.'); } + if (a.optionId === 'reroll' && a.register) { + console.log(`REGISTER: the user steered the next hand to the ${a.register} register. Re-run concept-seed with the same key, the next --reroll round, and --register ${a.register}, then follow what it prints; the register is the user's steering, never yours to pre-select.`); + } + if (a.followup && a.optionId !== 'reroll') { + console.log('FOLLOWUP OPEN: the table stays open and the page is showing a loading hand. Deliver the next round now with --update --key --payload , then collect it with --wait; never leave the page waiting on a round you have not sent.'); + } } catch { /* raw answer */ } } @@ -144,15 +173,17 @@ if (hasFlag('schema')) { title: 'Choose the visual world', question: 'The roll assigned Fillmore Handbill. Keep it, take an alternate, or re-roll.', options: [ - { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, - { id: 'challenger-teletext', label: 'Teletext Service', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, + { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, + { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/sketches/model-pick.webp' }, + { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, + { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, ], - reroll: true, + reroll: { registers: ['safer', 'bolder'] }, canon: true, canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', viewport: 'The arrangement a visitor expects, at full craft.', sketch: '.impeccable/sketches/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch 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 sketch, never as the promise of the build. 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. 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. Sketch aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either.'); + console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. 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. Sketch 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); } @@ -196,12 +227,16 @@ if (hasFlag('wait')) { if (!answered()) { console.log(`WAITING: no answer yet after ${pollSec}s; run --wait --key ${key} again`); process.exit(3); } const collected = fs.readFileSync(answerFile(key), 'utf8').trim(); printAnswer(collected); - // A re-roll keeps the table open: the server stays alive awaiting --update, - // so only the answer file is consumed. Terminal choices clean up fully. - let isRerollAnswer = false; - try { isRerollAnswer = JSON.parse(collected).optionId === 'reroll'; } catch { /* treat as terminal */ } + // A re-roll or a followup-round pick keeps the table open: the server stays + // alive awaiting --update, so only the answer file is consumed. Terminal + // choices clean up fully. + let keepsTableOpen = false; + try { + const parsedAnswer = JSON.parse(collected); + keepsTableOpen = parsedAnswer.optionId === 'reroll' || parsedAnswer.followup === true; + } catch { /* treat as terminal */ } try { fs.rmSync(answerFile(key)); } catch { /* already gone */ } - if (!isRerollAnswer) { try { fs.rmSync(stateFile(key)); } catch { /* already gone */ } } + if (!keepsTableOpen) { try { fs.rmSync(stateFile(key)); } catch { /* already gone */ } } process.exit(0); } @@ -302,6 +337,11 @@ function loadRound(json) { sketchSrc: sketchSrc(option.sketch), }); options = parsed.options.map(decorate); + // The verdict routes rendering: full cards first, declined cards demoted to + // the end of the deck in their own payload order. The reorder happens here + // so a payload that interleaves them still renders the weighing's shape. + const declined = options.filter((o) => o.verdict === 'declined'); + if (declined.length) options = [...options.filter((o) => o.verdict !== 'declined'), ...declined]; // The standing exit as a full card: same anatomy, reserved id, rendered // subordinate by the page. Without it, canon stays the quiet footer action. if (parsed.canonCard && typeof parsed.canonCard === 'object') { @@ -322,7 +362,19 @@ function page() { // and material tags give a text-only direction an immediate identity that // no generation luck can distort. const fact = (label, value, cls = '') => value ? `

${label}${esc(value)}

` : ''; - const hasMedia = (option) => Boolean(option.sketchSrc || option.heroSrc || option.boardSrc); + const demoted = (option) => option.verdict === 'declined'; + // Salience parity: a card's imagery weight is capped by the assigned card's. + // When the lead card has no media at all (no image generation this round, + // and no catalog art of its own), full-bleed catalog art beside a text-only + // assigned card would let rendering luck outvote the weighing: users click + // the colorful thing. Declined cards are thumb-only regardless; the verdict + // demoted them, and a full-bleed hero would promote them right back. + const identityRound = !(options[0] && (options[0].sketchSrc || options[0].heroSrc || options[0].boardSrc)); + // A declined card never renders a full media face, sketch included: even a + // declared sketch would buy back the salience the verdict took away. + const faceSketch = (option) => demoted(option) ? null : option.sketchSrc; + const thumbOnly = (option) => !faceSketch(option) && Boolean(option.heroSrc || option.boardSrc) && (demoted(option) || identityRound); + const hasMedia = (option) => Boolean(faceSketch(option) || ((option.heroSrc || option.boardSrc) && !thumbOnly(option))); // The back exists to keep long facts off a card whose front is an image; // a card with no art has no flip chip to reach it, so it gets no back and // the full read lives on the front instead. @@ -338,6 +390,19 @@ function page() { idBits.push(option.materials.slice(0, 4).map((m) => `${esc(m)}`).join('')); } if (idBits.length) rows.push(`
${idBits.join('')}
`); + // Donations from declined challengers render as named raise lines: the + // assigned card arrives already raised by the hand it beat, and the raise + // is readable, because a raise nobody can read did not happen. + if (Array.isArray(option.raised) && option.raised.length) { + const nameOf = (id) => options.find((o) => o.id === id)?.label || String(id ?? ''); + rows.push(`
${option.raised.slice(0, 4).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`).join('')}
`); + } + // Demoted art stays reachable as a labeled thumb: the catalog world + // explains where the direction comes from without buying it back the + // salience the verdict took away. + if (thumbOnly(option)) { + rows.push(`
inspired by
`); + } // The front carries only what the choice needs: thesis, identity, and the // honest risk clamped to two lines. First viewport and the case read on // the card's back; once the sketch lands, the first viewport is a picture. @@ -348,6 +413,7 @@ function page() { } else { rows.push(fact('First viewport', option.viewport)); rows.push(fact('The case', option.case)); + rows.push(fact('Kept', option.kept)); rows.push(fact('Risk', option.risk)); } if (!option.thesis && option.body) rows.push(`

${esc(option.body)}

`); @@ -357,6 +423,7 @@ function page() { const backFacts = (option) => [ fact('First viewport', option.viewport), fact('The case', option.case), + fact('Kept', option.kept), fact('Risk', option.risk), option.body && option.thesis ? `

${esc(option.body)}

` : '', ].filter(Boolean).join('\n '); @@ -366,7 +433,10 @@ function page() {
inspiration
` : ''; const details = hasBack(option) ? flipChip('Details') : ''; - if (option.sketchSrc) { + // Thumb-only art renders inside the body via anatomy(), never as a face, + // and a declined card's sketch slot is ignored outright. + if (thumbOnly(option)) return ''; + if (faceSketch(option)) { return `
sketching…
@@ -385,17 +455,18 @@ function page() { } 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)}` : option.isCanon ? 'The standing door' : ''} + ${option.kicker ? `${esc(option.kicker)}` : demoted(option) ? 'Declined' : option.isCanon ? 'The standing door' : ''} ${media(option)}
${option.lineage ? `

${esc(option.lineage)}

` : ''}

${esc(option.label)}

${anatomy(option)} - +
${hasBack(option) ? `
@@ -406,7 +477,7 @@ function page() {
${option.boardSrc ? `

The full read · ${esc(option.label)}

` : ''} ${backFacts(option)} - +
` : ''}
@@ -585,6 +656,27 @@ function page() { /* The generic .media img display:block would defeat [hidden] and float an empty block over the shimmer; an unloaded sketch must truly not render. */ .media img[hidden] { display: none; } + /* Declined challengers: the weighing demoted them, so the card is narrower + and quieter, its catalog art rides as a labeled thumb in the body, and + the action reads "Adopt anyway". Adoptable, never deleted: the demoted + row is the hand's proof of judgment. */ + .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); } + .card.declined .face { background: var(--ks-graphite); } + .card.declined:hover .face { border-color: var(--ks-text-faint); } + .card.declined h2 { font-size: 1rem; color: var(--ks-text); } + .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); } + /* 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); } + .inspo img { display: block; width: 100%; height: 100%; object-fit: cover; } + .inspo figcaption { position: absolute; left: 0; right: 0; bottom: 0; font-family: var(--ks-mono); font-size: .48rem; letter-spacing: .16em; text-transform: uppercase; color: var(--ks-text); text-align: center; padding: 2px 0 3px; background: oklch(7% 0.006 95 / 0.72); } + /* Raises: the donations the assigned direction took from the hand it beat, + each named for its donor. Patina, not kinpaku: a raise is provenance. */ + .raises { display: flex; flex-direction: column; gap: 4px; margin: 2px 0; } + .raise { font-size: .78rem; color: var(--ks-text-muted); line-height: 1.45; border-left: 2px solid var(--ks-patina); padding-left: 8px; } + .raise .fact-label { color: var(--ks-patina); } /* The standing exit as a card: present with full anatomy, never dressed as a contender. Graphite instead of kinpaku, and it never takes the lead ring. */ .card.canon .face { border-color: var(--ks-rule); background: var(--ks-graphite); } @@ -597,9 +689,14 @@ function page() { footer { width: 100%; max-width: 90rem; margin: 1.6rem auto 0; display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; } #steer { flex: 1; min-width: 16rem; background: var(--ks-lacquer-raised); color: var(--ks-text); border: 1px solid var(--ks-rule); border-radius: 7px; padding: .6rem .85rem; font: inherit; } #steer:focus { outline: none; border-color: var(--ks-patina); } - #reroll { display: inline-flex; align-items: center; align-self: stretch; gap: 8px; padding: 0 16px; font-family: var(--ks-mono); font-size: .72rem; letter-spacing: .08em; text-transform: uppercase; color: var(--ks-kinpaku); background: transparent; border: 1px solid var(--ks-rule); border-radius: 6px; cursor: pointer; transition: border-color .2s ease, color .2s ease; } - #reroll:hover { color: var(--ks-kinpaku-pale); border-color: var(--ks-kinpaku-deep); } - #reroll svg { width: 15px; height: 15px; } + .reroll-btn { display: inline-flex; align-items: center; align-self: stretch; gap: 8px; padding: 0 16px; font-family: var(--ks-mono); font-size: .72rem; letter-spacing: .08em; text-transform: uppercase; color: var(--ks-kinpaku); background: transparent; border: 1px solid var(--ks-rule); border-radius: 6px; cursor: pointer; transition: border-color .2s ease, color .2s ease; } + .reroll-btn:hover { color: var(--ks-kinpaku-pale); border-color: var(--ks-kinpaku-deep); } + .reroll-btn svg { width: 15px; height: 15px; } + .reroll-btn[disabled] { opacity: .4; cursor: default; } + /* The register steers read quieter than the plain roll: they are exits from + the current register, not the round's main verbs. */ + #reroll-safer, #reroll-bolder { color: var(--ks-text-muted); min-height: 38px; } + #reroll-safer:hover, #reroll-bolder:hover { color: var(--ks-text); border-color: var(--ks-text-faint); } /* The quiet exit: always available, never argued with, visually subordinate to the dealt cards and the re-roll so it reads as the user's own door, not a recommendation. */ @@ -644,16 +741,33 @@ function page() {
${payload.steer ? '' : ''} - ${payload.reroll ? '' : ''} + ${(() => { + if (!payload.reroll) return ''; + const die = ''; + const registers = Array.isArray(payload.reroll.registers) ? payload.reroll.registers.filter((r) => r === 'safer' || r === 'bolder') : []; + // The registers are the user's steering wheel on the familiar-to-bold + // axis; the plain re-roll sits between them so the spatial order matches + // the axis it names. + const safer = registers.includes('safer') ? '' : ''; + const bolder = registers.includes('bolder') ? '' : ''; + return `${safer}${bolder}`; + })()} ${payload.canon && !payload.canonCard ? '' : ''}
`; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/tests/concept-seed.test.mjs b/tests/concept-seed.test.mjs index 2717dd849..e1963f84c 100644 --- a/tests/concept-seed.test.mjs +++ b/tests/concept-seed.test.mjs @@ -11,7 +11,7 @@ import { validateConceptEntry, } from '../skill/scripts/lib/concept-catalog.mjs'; import { readCompositionCatalog } from '../skill/scripts/lib/composition-catalog.mjs'; -import { dealCompositions, renderChallenger, selectApprovedChallengers, selectApprovedComposition, selectApprovedCompositions } from '../skill/scripts/concept-seed.mjs'; +import { dealCompositions, pingChosen, renderChallenger, selectApprovedChallengers, selectApprovedComposition, selectApprovedCompositions } from '../skill/scripts/concept-seed.mjs'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const SCRIPT = path.join(ROOT, 'skill', 'scripts', 'concept-seed.mjs'); @@ -251,6 +251,54 @@ describe('concept seed scopes', () => { assert.match(invalid.stderr, /non-negative integer/); }); + it('registers steer the round presentation without changing the deal', () => { + const plain = run('direction', ['--reroll', '1']); + const bolder = run('direction', ['--reroll', '1', '--register', 'bolder']); + const safer = run('direction', ['--reroll', '1', '--register', 'safer']); + assert.equal(bolder.status, 0); + assert.equal(safer.status, 0); + // A register is presentation-only: the same key and reroll count deal the + // same challengers, so the exclusion chain never forks on register. + const dealtIds = (out) => [...out.matchAll(/SOURCE ID: ([a-z0-9-]+)/g)].map((m) => m[1]).sort(); + assert.deepEqual(dealtIds(bolder.stdout), dealtIds(plain.stdout), 'bolder presents the same deal the plain round drew'); + assert.match(bolder.stdout, /BOLDER REGISTER/); + assert.match(bolder.stdout, /FIRST dealt challenger leads/); + assert.match(bolder.stdout, /--register bolder/); + assert.doesNotMatch(bolder.stdout, /ASSIGNED INDEX:/); + // The generic weighing instruction measures against the assigned + // direction, which a bolder round suspended; bolder weighs against the + // leader instead, and the contradiction must not ship. + assert.match(bolder.stdout, /against the fused LEADER/); + assert.doesNotMatch(bolder.stdout, /against the assigned direction/); + assert.match(safer.stdout, /SAFER REGISTER/); + assert.match(safer.stdout, /sanctioned lineup/); + assert.doesNotMatch(safer.stdout, /^CHALLENGERS:/m, 'the safer round spends its hand unseen'); + // Degraded safer must not contradict itself: "the user picks" and a + // mandatory numbered build order cannot share one output. + const degradedSafer = run('direction', ['--reroll', '1', '--register', 'safer'], { + IMPECCABLE_CATALOG_DIR: '/nonexistent-catalog-dir', + IMPECCABLE_API_URL: 'http://127.0.0.1:1', + }); + assert.equal(degradedSafer.status, 0); + assert.match(degradedSafer.stdout, /source: degraded/); + assert.match(degradedSafer.stdout, /SAFER REGISTER/); + assert.doesNotMatch(degradedSafer.stdout, /ASSIGNED INDEX/, 'degraded safer suppresses the assignment machinery'); + assert.doesNotMatch(degradedSafer.stdout, /Build candidate/, 'degraded safer mandates no numbered candidate'); + const degradedBolder = run('direction', ['--reroll', '1', '--register', 'bolder'], { + IMPECCABLE_CATALOG_DIR: '/nonexistent-catalog-dir', + IMPECCABLE_API_URL: 'http://127.0.0.1:1', + }); + assert.equal(degradedBolder.status, 0); + assert.match(degradedBolder.stdout, /BOLDER REGISTER UNAVAILABLE/); + assert.match(degradedBolder.stdout, /ASSIGNED INDEX: /, 'degraded bolder falls back to the plain grounded assignment'); + const invalidRegister = run('direction', ['--reroll', '1', '--register', 'wilder']); + assert.notEqual(invalidRegister.status, 0); + assert.match(invalidRegister.stderr, /must be safer or bolder/); + const noReroll = run('direction', ['--register', 'bolder']); + assert.notEqual(noReroll.status, 0); + assert.match(noReroll.stderr, /re-roll round/); + }); + it('filters challengers by strength per scope and falls back when a tier has no match', () => { const make = (id, tier, strength) => ({ id, @@ -481,6 +529,50 @@ describe('init gate', () => { }); assert.equal(result.status, 0); assert.doesNotMatch(result.stdout, /NO_PRODUCT_MD/); + // --kind alone is a valid ping invocation (assigned/pick/canon outcomes + // have no catalog id) and is equally ungated. + const kindOnly = spawnSync(process.execPath, [SCRIPT, '--kind', 'assigned', '--from', 'gate-test'], { + cwd: dir, + encoding: 'utf-8', + env: { ...process.env, IMPECCABLE_CATALOG_DIR: FIXTURE_DIR, IMPECCABLE_NO_TELEMETRY: '1' }, + }); + assert.equal(kindOnly.status, 0); + assert.doesNotMatch(kindOnly.stdout, /NO_PRODUCT_MD/); + assert.match(kindOnly.stdout, /choice ping skipped/, 'telemetry-disabled kind ping reports skipped, not an error'); + }); + + it('pingChosen validates kinds, requires ids only for challenger wins, and honors opt-out', async () => { + const calls = []; + const realFetch = globalThis.fetch; + globalThis.fetch = async (url, opts) => { calls.push(JSON.parse(opts.body)); return { ok: true }; }; + // telemetryDisabled() honors DO_NOT_TRACK too, so a developer shell with + // it set must not fail the success-path assertions below. + const savedDnt = process.env.DO_NOT_TRACK; + const savedNoTelemetry = process.env.IMPECCABLE_NO_TELEMETRY; + try { + delete process.env.DO_NOT_TRACK; + process.env.IMPECCABLE_NO_TELEMETRY = '1'; + assert.equal(await pingChosen({ kind: 'assigned', key: 'k' }), false, 'opt-out wins over everything'); + delete process.env.IMPECCABLE_NO_TELEMETRY; + assert.equal(await pingChosen({ kind: 'assigned', key: 'k', scope: 'direction' }), true, 'kind-only ping for a non-challenger outcome'); + assert.equal(await pingChosen({ kind: 'challenger', key: 'k' }), false, 'a challenger win without an id is not a ping'); + assert.equal(await pingChosen({ kind: 'weird', chosenId: 'x', key: 'k' }), false, 'unknown kinds are dropped'); + assert.equal(await pingChosen({ kind: 'assigned', register: 'wilder', key: 'k' }), false, 'unknown registers are dropped'); + assert.equal(await pingChosen({ chosenId: 'legacy-id', key: 'k' }), true, 'legacy id-only shape stays valid'); + assert.equal(await pingChosen({ kind: 'canon', register: 'safer', key: 'k' }), true, 'register rides along on a steered round'); + const bodies = calls; + assert.equal(bodies[0].kind, 'assigned'); + assert.equal(bodies[0].chosenId, undefined, 'no id field on kind-only pings'); + assert.equal(bodies[1].chosenId, 'legacy-id'); + assert.equal(bodies[1].kind, undefined, 'legacy pings carry no kind'); + assert.equal(bodies[2].register, 'safer'); + } finally { + globalThis.fetch = realFetch; + if (savedDnt === undefined) delete process.env.DO_NOT_TRACK; + else process.env.DO_NOT_TRACK = savedDnt; + if (savedNoTelemetry === undefined) delete process.env.IMPECCABLE_NO_TELEMETRY; + else process.env.IMPECCABLE_NO_TELEMETRY = savedNoTelemetry; + } }); // Mode eligibility on worlds. Before this, selectApprovedChallengers never diff --git a/tests/new-work-e2e.test.mjs b/tests/new-work-e2e.test.mjs index 327cf07d8..067c2c002 100644 --- a/tests/new-work-e2e.test.mjs +++ b/tests/new-work-e2e.test.mjs @@ -267,7 +267,7 @@ describe('new-work-e2e: serve-question decision page', () => { } }); - it('(e) an option with no hero renders a text-only card (no .media element)', async () => { + it('(e) a text-only assigned card caps every card at thumb imagery (salience parity)', async () => { const cwd = makeWorkspace(); const key = 'textonly'; const hero = makeFakeImage(cwd, 'has a hero', 'hero.png'); @@ -295,17 +295,173 @@ describe('new-work-e2e: serve-question decision page', () => { // on the front, where nothing else would have used the room. const textOnlyBack = await page.$('.card[data-id="assigned"] .face.back'); const frontRead = await page.$eval('.card[data-id="assigned"] .face.front', (el) => el.textContent); - // Catalog art without a sketch is labeled as reference, never as the - // promise of the build. - const heroLabel = await page.$eval('.card[data-id="challenger-hero"] .media .media-label', (el) => el.textContent); + // Salience parity: with a text-only assigned card, a challenger's + // catalog art may not render as a full-bleed face beside it; it demotes + // to a labeled thumb in the body, so pretty pixels never outvote the + // weighing. The thumb stays labeled as reference, never as the promise + // of the build. + const heroThumbLabel = await page.$eval('.card[data-id="challenger-hero"] .inspo figcaption', (el) => el.textContent); await context.close(); assert.equal(textOnlyMedia, null, 'text-only card has no .media region'); assert.ok(textOnlyFace, 'text-only card carries the .text-only face class'); - assert.ok(heroMedia, 'the hero card still renders its .media region'); + assert.equal(heroMedia, null, 'parity demotes the challenger hero from a full-bleed face'); assert.equal(textOnlyBack, null, 'text-only card has no unreachable back face'); assert.match(frontRead, /First viewport/, 'text-only front carries the first viewport fact'); assert.match(frontRead, /The case/, 'text-only front carries the case fact'); - assert.equal(heroLabel, 'inspiration', 'sketchless catalog art is labeled inspiration'); + assert.equal(heroThumbLabel, 'inspired by', 'demoted catalog art is a labeled thumb'); + } finally { + await stopDaemon(cwd, key); + rmSync(cwd, { recursive: true, force: true }); + } + }); + + it('(e2) verdicts route the deck: declined cards demote, reorder to the end, and stay adoptable', async () => { + const cwd = makeWorkspace(); + const key = 'verdicts'; + const hero = makeFakeImage(cwd, 'declined hero', 'declined-hero.png'); + const winnerHero = makeFakeImage(cwd, 'winner hero', 'winner-hero.png'); + const payload = { + title: 'Choose the visual world', + options: [ + { + id: 'assigned', label: 'The Seedsman Catalog', kicker: 'THE ROLL', hero: winnerHero, + raised: [{ from: 'challenger-deepsea', raise: 'The catalog now owns its whole viewport.' }], + }, + // Declined dealt before a competitive card on purpose: the page owns + // the reorder, so payload order cannot promote a demoted world. + { + id: 'challenger-deepsea', label: 'Deep Sea Survey', verdict: 'declined', + case: 'Fuses poorly: buyers do not identify with abyssal instrumentation.', + kept: 'Total environmental commitment.', hero, + // A stray sketch on a declined card must not re-promote it to a + // full media face; the renderer ignores it outright. + sketch: '.impeccable/sketches/challenger-deepsea.webp', + }, + { id: 'challenger-waxprint', label: 'Wax Print Market', verdict: 'competitive', hero: winnerHero }, + ], + reroll: true, steer: true, + }; + const { url } = await startDaemon(cwd, payload, key); + try { + const context = await browser.newContext(); + const page = await context.newPage(); + await page.goto(url, { waitUntil: 'load' }); + await page.waitForSelector('button.choose'); + const order = await page.$$eval('.card', (cards) => cards.map((c) => c.dataset.id)); + const declinedCard = await page.$('.card.declined[data-id="challenger-deepsea"]'); + const declinedMedia = await page.$('.card[data-id="challenger-deepsea"] .media'); + const declinedThumb = await page.$('.card[data-id="challenger-deepsea"] .inspo'); + const declinedFront = await page.$eval('.card[data-id="challenger-deepsea"] .face.front', (el) => el.textContent); + const declinedButton = await page.$eval('.card[data-id="challenger-deepsea"] .face.front button.choose', (el) => el.textContent); + const competitiveMedia = await page.$('.card[data-id="challenger-waxprint"] .media'); + const raise = await page.$eval('.card[data-id="assigned"] .raise', (el) => el.textContent); + // A demoted card is still a real choice: adopting it must answer. + await page.click('.card[data-id="challenger-deepsea"] .face.front button.choose'); + const collected = await waitLoop(cwd, key); + await context.close(); + assert.equal(collected.code, 0, collected.out); + const answer = JSON.parse(collected.out.match(/ANSWER: (\{.*\})/)[1]); + assert.deepEqual(order, ['assigned', 'challenger-waxprint', 'challenger-deepsea'], 'declined cards reorder to the end'); + assert.ok(declinedCard, 'declined verdict adds the .declined card class'); + assert.equal(declinedMedia, null, 'declined catalog art never renders full-bleed'); + assert.ok(declinedThumb, 'declined catalog art rides as a labeled thumb'); + assert.match(declinedFront, /Kept/, 'the declined front carries its kept line'); + assert.equal(declinedButton, 'Adopt anyway', 'the declined action is adopt, not build'); + assert.ok(competitiveMedia, 'a competitive challenger keeps its full media face'); + assert.match(raise, /Raised by Deep Sea Survey/, 'the assigned card names its donor'); + assert.equal(answer.optionId, 'challenger-deepsea', 'adopting a declined card answers with its id'); + } finally { + await stopDaemon(cwd, key); + rmSync(cwd, { recursive: true, force: true }); + } + }); + + it('(e3) register steers ride the re-roll: the bolder button answers with its register', async () => { + const cwd = makeWorkspace(); + const key = 'registers'; + const payload = { + title: 'Choose the visual world', + options: [{ id: 'assigned', label: 'The Seedsman Catalog', kicker: 'THE ROLL' }], + reroll: { registers: ['safer', 'bolder'] }, + steer: true, + }; + const { url } = await startDaemon(cwd, payload, key); + try { + const context = await browser.newContext(); + const page = await context.newPage(); + await page.goto(url, { waitUntil: 'load' }); + await page.waitForSelector('#reroll-bolder'); + assert.ok(await page.$('#reroll-safer'), 'the safer steer renders'); + assert.ok(await page.$('#reroll'), 'the plain re-roll stays between the registers'); + await page.click('#reroll-bolder'); + const collected = await waitLoop(cwd, key); + await context.close(); + assert.equal(collected.code, 0, collected.out); + const answer = JSON.parse(collected.out.match(/ANSWER: (\{.*\})/)[1]); + assert.equal(answer.optionId, 'reroll'); + assert.equal(answer.register, 'bolder', 'the answer names the requested register'); + assert.match(collected.out, /REGISTER: .*bolder/, 'the register directive tells the agent to re-run the seed'); + } finally { + await stopDaemon(cwd, key); + rmSync(cwd, { recursive: true, force: true }); + } + }); + + it('(e4) followup keeps the table open: direction pick, then the execution-contract round', async () => { + const cwd = makeWorkspace(); + const key = 'followup'; + const directionRound = { + title: 'Choose the visual world', + options: [{ id: 'assigned', label: 'The Seedsman Catalog', kicker: 'THE ROLL' }], + reroll: true, steer: true, + followup: true, + }; + const buildPathRound = { + title: 'How should it be built?', + question: 'Same direction, two execution contracts. Pick where the risk goes.', + options: [ + { id: 'comp-led', label: 'Comp-led', kicker: 'BOLD', thesis: 'A first-viewport comp leads and the build matches it.', risk: 'Fix rounds expected; motion arrives last.' }, + { id: 'code-led', label: 'Code-led', kicker: 'CRAFTED', thesis: 'Code authors the page; the boards calibrate it.', risk: 'Composition stays closer to convention.' }, + ], + }; + const { url } = await startDaemon(cwd, directionRound, key); + try { + const context = await browser.newContext(); + const page = await context.newPage(); + await page.goto(url, { waitUntil: 'load' }); + await page.waitForSelector('button.choose'); + await page.click('.card[data-id="assigned"] .face.front button.choose'); + + // First answer: the direction. Not terminal, and it says so. + const first = await waitLoop(cwd, key); + assert.equal(first.code, 0, first.out); + const firstAnswer = JSON.parse(first.out.match(/ANSWER: (\{.*\})/)[1]); + assert.equal(firstAnswer.optionId, 'assigned'); + assert.equal(firstAnswer.followup, true, 'the answer marks the table as still open'); + assert.match(first.out, /FOLLOWUP OPEN:/, 'the directive tells the agent to send the next round'); + assert.ok(existsSync(path.join(cwd, '.impeccable', 'questions', `${key}.state.json`)), + 'server state file survives a followup pick'); + + // The page swapped to the loading hand instead of goodbye. + await page.waitForSelector('.card.skeleton'); + + // Deliver the execution-contract round; the page reloads into it. + const nextPayloadPath = path.join(cwd, 'build-path.json'); + writeFileSync(nextPayloadPath, JSON.stringify(buildPathRound)); + const updated = await run(['--update', '--key', key, '--payload', nextPayloadPath], cwd); + assert.equal(updated.code, 0, updated.out); + await page.waitForSelector('.card[data-id="code-led"]'); + await page.click('.card[data-id="code-led"] .face.front button.choose'); + + // Second answer: terminal, table cleaned up. + const second = await waitLoop(cwd, key); + await context.close(); + assert.equal(second.code, 0, second.out); + const secondAnswer = JSON.parse(second.out.match(/ANSWER: (\{.*\})/)[1]); + assert.equal(secondAnswer.optionId, 'code-led'); + assert.equal(secondAnswer.followup, undefined, 'the build-path round is terminal'); + assert.ok(!existsSync(path.join(cwd, '.impeccable', 'questions', `${key}.state.json`)), + 'the terminal pick removes the server state file'); } finally { await stopDaemon(cwd, key); rmSync(cwd, { recursive: true, force: true }); diff --git a/tests/serve-question.test.mjs b/tests/serve-question.test.mjs index cfee001b0..f6dd97a53 100644 --- a/tests/serve-question.test.mjs +++ b/tests/serve-question.test.mjs @@ -208,11 +208,16 @@ describe('serve-question', () => { canon: true, canonCard: { label: 'The category standard', thesis: 'What the category ships.' }, steer: true, + followup: true, }; const { child, url, read } = await startServer(payload); const html = await (await fetch(url)).text(); // Anatomy renders: chips, tags, fact labels, thesis. assert.match(html, /swatches/); + // A blocking server has no update channel, so even a followup payload + // must not arm the page's loading-hand path (detached mode arms it; the + // new-work e2e suite covers that side). + assert.match(html, /const FOLLOWUP = false/); assert.match(html, /background:#e8452c/); assert.match(html, /class="tag">letterpress/); assert.match(html, /The gig poster idea\./); From d65a08b0644bc2bc9639fcf22b042eadf4136f2d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 21:17:56 +0000 Subject: [PATCH 02/29] Sync generated provider output --- .agents/skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .claude/skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .cursor/skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .gemini/skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .github/skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .grok/skills/impeccable/reference/bolder.md | 2 + .grok/skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .kiro/skills/impeccable/reference/bolder.md | 2 + .kiro/skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .../skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .pi/skills/impeccable/reference/bolder.md | 2 + .pi/skills/impeccable/reference/new-work.md | 12 +- .pi/skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .pi/skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .qoder/skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .../skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .../skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .trae/skills/impeccable/reference/bolder.md | 2 + .trae/skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .vibe/skills/impeccable/reference/bolder.md | 2 + .vibe/skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- plugin/skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- 135 files changed, 7365 insertions(+), 2175 deletions(-) create mode 100644 .agents/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .claude/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .cursor/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .gemini/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .github/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .grok/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .kiro/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .opencode/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .pi/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .qoder/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .rovodev/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .trae-cn/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .trae/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .vibe/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 plugin/skills/impeccable/scripts/live/ui-surfaces.mjs diff --git a/.agents/skills/impeccable/reference/bolder.md b/.agents/skills/impeccable/reference/bolder.md index 9fe39ca59..026c10a6a 100644 --- a/.agents/skills/impeccable/reference/bolder.md +++ b/.agents/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.agents/skills/impeccable/reference/new-work.md b/.agents/skills/impeccable/reference/new-work.md index 755d734e3..8f654b7d7 100644 --- a/.agents/skills/impeccable/reference/new-work.md +++ b/.agents/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .agents/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .agents/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .agents/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .agents/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.agents/skills/impeccable/reference/visualize.md b/.agents/skills/impeccable/reference/visualize.md index 815e29d3a..c20d910f3 100644 --- a/.agents/skills/impeccable/reference/visualize.md +++ b/.agents/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. 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. 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. diff --git a/.agents/skills/impeccable/scripts/concept-seed.mjs b/.agents/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.agents/skills/impeccable/scripts/concept-seed.mjs +++ b/.agents/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only 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, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${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. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - 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}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${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. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a 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. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; 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) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}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}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.agents/skills/impeccable/scripts/lib/impeccable-config.mjs b/.agents/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.agents/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.agents/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.agents/skills/impeccable/scripts/live-browser.js b/.agents/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.agents/skills/impeccable/scripts/live-browser.js +++ b/.agents/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.agents/skills/impeccable/scripts/live/browser-script-parts.mjs b/.agents/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.agents/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.agents/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.agents/skills/impeccable/scripts/live/ui-surfaces.mjs b/.agents/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.agents/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.claude/skills/impeccable/reference/bolder.md b/.claude/skills/impeccable/reference/bolder.md index fced49456..a5c34cd3e 100644 --- a/.claude/skills/impeccable/reference/bolder.md +++ b/.claude/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.claude/skills/impeccable/reference/new-work.md b/.claude/skills/impeccable/reference/new-work.md index 5161d25cb..718b4bd0b 100644 --- a/.claude/skills/impeccable/reference/new-work.md +++ b/.claude/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .claude/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .claude/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .claude/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .claude/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -80,7 +82,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.claude/skills/impeccable/reference/visualize.md b/.claude/skills/impeccable/reference/visualize.md index 94c337f15..6df08b30b 100644 --- a/.claude/skills/impeccable/reference/visualize.md +++ b/.claude/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. 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. 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. diff --git a/.claude/skills/impeccable/scripts/concept-seed.mjs b/.claude/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.claude/skills/impeccable/scripts/concept-seed.mjs +++ b/.claude/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only 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, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${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. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - 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}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${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. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a 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. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; 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) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}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}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.claude/skills/impeccable/scripts/lib/impeccable-config.mjs b/.claude/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.claude/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.claude/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.claude/skills/impeccable/scripts/live-browser.js b/.claude/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.claude/skills/impeccable/scripts/live-browser.js +++ b/.claude/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.claude/skills/impeccable/scripts/live/browser-script-parts.mjs b/.claude/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.claude/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.claude/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.claude/skills/impeccable/scripts/live/ui-surfaces.mjs b/.claude/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.claude/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.cursor/skills/impeccable/reference/bolder.md b/.cursor/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.cursor/skills/impeccable/reference/bolder.md +++ b/.cursor/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.cursor/skills/impeccable/reference/new-work.md b/.cursor/skills/impeccable/reference/new-work.md index 99ed558b5..0dff0aea7 100644 --- a/.cursor/skills/impeccable/reference/new-work.md +++ b/.cursor/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .cursor/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .cursor/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .cursor/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .cursor/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.cursor/skills/impeccable/reference/visualize.md b/.cursor/skills/impeccable/reference/visualize.md index 5877eae67..3a8cc1e4a 100644 --- a/.cursor/skills/impeccable/reference/visualize.md +++ b/.cursor/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. 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. 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. diff --git a/.cursor/skills/impeccable/scripts/concept-seed.mjs b/.cursor/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.cursor/skills/impeccable/scripts/concept-seed.mjs +++ b/.cursor/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only 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, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${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. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - 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}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${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. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a 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. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; 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) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}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}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.cursor/skills/impeccable/scripts/lib/impeccable-config.mjs b/.cursor/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.cursor/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.cursor/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.cursor/skills/impeccable/scripts/live-browser.js b/.cursor/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.cursor/skills/impeccable/scripts/live-browser.js +++ b/.cursor/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.cursor/skills/impeccable/scripts/live/browser-script-parts.mjs b/.cursor/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.cursor/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.cursor/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.cursor/skills/impeccable/scripts/live/ui-surfaces.mjs b/.cursor/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.cursor/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.gemini/skills/impeccable/reference/bolder.md b/.gemini/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.gemini/skills/impeccable/reference/bolder.md +++ b/.gemini/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.gemini/skills/impeccable/reference/new-work.md b/.gemini/skills/impeccable/reference/new-work.md index 77ec7396f..08a8ed6f0 100644 --- a/.gemini/skills/impeccable/reference/new-work.md +++ b/.gemini/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .gemini/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .gemini/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .gemini/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .gemini/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.gemini/skills/impeccable/reference/visualize.md b/.gemini/skills/impeccable/reference/visualize.md index 6dae2cb0b..82f2cc4de 100644 --- a/.gemini/skills/impeccable/reference/visualize.md +++ b/.gemini/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. 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. 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. diff --git a/.gemini/skills/impeccable/scripts/concept-seed.mjs b/.gemini/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.gemini/skills/impeccable/scripts/concept-seed.mjs +++ b/.gemini/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only 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, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${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. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - 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}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${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. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a 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. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; 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) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}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}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.gemini/skills/impeccable/scripts/lib/impeccable-config.mjs b/.gemini/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.gemini/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.gemini/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.gemini/skills/impeccable/scripts/live-browser.js b/.gemini/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.gemini/skills/impeccable/scripts/live-browser.js +++ b/.gemini/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.gemini/skills/impeccable/scripts/live/browser-script-parts.mjs b/.gemini/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.gemini/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.gemini/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.gemini/skills/impeccable/scripts/live/ui-surfaces.mjs b/.gemini/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.gemini/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.github/skills/impeccable/reference/bolder.md b/.github/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.github/skills/impeccable/reference/bolder.md +++ b/.github/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.github/skills/impeccable/reference/new-work.md b/.github/skills/impeccable/reference/new-work.md index a249c27ad..959eb081e 100644 --- a/.github/skills/impeccable/reference/new-work.md +++ b/.github/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .github/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .github/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .github/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .github/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.github/skills/impeccable/reference/visualize.md b/.github/skills/impeccable/reference/visualize.md index 285a4c996..60ee2020a 100644 --- a/.github/skills/impeccable/reference/visualize.md +++ b/.github/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. 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. 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. diff --git a/.github/skills/impeccable/scripts/concept-seed.mjs b/.github/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.github/skills/impeccable/scripts/concept-seed.mjs +++ b/.github/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only 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, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${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. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - 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}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${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. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a 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. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; 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) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}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}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.github/skills/impeccable/scripts/lib/impeccable-config.mjs b/.github/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.github/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.github/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.github/skills/impeccable/scripts/live-browser.js b/.github/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.github/skills/impeccable/scripts/live-browser.js +++ b/.github/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.github/skills/impeccable/scripts/live/browser-script-parts.mjs b/.github/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.github/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.github/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.github/skills/impeccable/scripts/live/ui-surfaces.mjs b/.github/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.github/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.grok/skills/impeccable/reference/bolder.md b/.grok/skills/impeccable/reference/bolder.md index fced49456..a5c34cd3e 100644 --- a/.grok/skills/impeccable/reference/bolder.md +++ b/.grok/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.grok/skills/impeccable/reference/new-work.md b/.grok/skills/impeccable/reference/new-work.md index 45027be6e..0e92394b7 100644 --- a/.grok/skills/impeccable/reference/new-work.md +++ b/.grok/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .grok/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .grok/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .grok/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .grok/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.grok/skills/impeccable/reference/visualize.md b/.grok/skills/impeccable/reference/visualize.md index 4dd8b3525..063520a6e 100644 --- a/.grok/skills/impeccable/reference/visualize.md +++ b/.grok/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. 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. 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. diff --git a/.grok/skills/impeccable/scripts/concept-seed.mjs b/.grok/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.grok/skills/impeccable/scripts/concept-seed.mjs +++ b/.grok/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only 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, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${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. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - 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}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${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. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a 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. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; 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) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}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}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.grok/skills/impeccable/scripts/lib/impeccable-config.mjs b/.grok/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.grok/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.grok/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.grok/skills/impeccable/scripts/live-browser.js b/.grok/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.grok/skills/impeccable/scripts/live-browser.js +++ b/.grok/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.grok/skills/impeccable/scripts/live/browser-script-parts.mjs b/.grok/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.grok/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.grok/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.grok/skills/impeccable/scripts/live/ui-surfaces.mjs b/.grok/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.grok/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.kiro/skills/impeccable/reference/bolder.md b/.kiro/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.kiro/skills/impeccable/reference/bolder.md +++ b/.kiro/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.kiro/skills/impeccable/reference/new-work.md b/.kiro/skills/impeccable/reference/new-work.md index 479e37b10..2402bc90a 100644 --- a/.kiro/skills/impeccable/reference/new-work.md +++ b/.kiro/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .kiro/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .kiro/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .kiro/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .kiro/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.kiro/skills/impeccable/reference/visualize.md b/.kiro/skills/impeccable/reference/visualize.md index f298c6c69..cc0057342 100644 --- a/.kiro/skills/impeccable/reference/visualize.md +++ b/.kiro/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. 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. 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. diff --git a/.kiro/skills/impeccable/scripts/concept-seed.mjs b/.kiro/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.kiro/skills/impeccable/scripts/concept-seed.mjs +++ b/.kiro/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only 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, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${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. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - 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}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${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. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a 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. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; 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) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}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}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.kiro/skills/impeccable/scripts/lib/impeccable-config.mjs b/.kiro/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.kiro/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.kiro/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.kiro/skills/impeccable/scripts/live-browser.js b/.kiro/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.kiro/skills/impeccable/scripts/live-browser.js +++ b/.kiro/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.kiro/skills/impeccable/scripts/live/browser-script-parts.mjs b/.kiro/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.kiro/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.kiro/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.kiro/skills/impeccable/scripts/live/ui-surfaces.mjs b/.kiro/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.kiro/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.opencode/skills/impeccable/reference/bolder.md b/.opencode/skills/impeccable/reference/bolder.md index 5408e49d0..1055d6a9f 100644 --- a/.opencode/skills/impeccable/reference/bolder.md +++ b/.opencode/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.opencode/skills/impeccable/reference/new-work.md b/.opencode/skills/impeccable/reference/new-work.md index 290c53138..95aa35654 100644 --- a/.opencode/skills/impeccable/reference/new-work.md +++ b/.opencode/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .opencode/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .opencode/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .opencode/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .opencode/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.opencode/skills/impeccable/reference/visualize.md b/.opencode/skills/impeccable/reference/visualize.md index 43aebde6e..5bd14ae50 100644 --- a/.opencode/skills/impeccable/reference/visualize.md +++ b/.opencode/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. 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. 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. diff --git a/.opencode/skills/impeccable/scripts/concept-seed.mjs b/.opencode/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.opencode/skills/impeccable/scripts/concept-seed.mjs +++ b/.opencode/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only 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, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${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. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - 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}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${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. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a 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. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; 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) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}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}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.opencode/skills/impeccable/scripts/lib/impeccable-config.mjs b/.opencode/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.opencode/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.opencode/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.opencode/skills/impeccable/scripts/live-browser.js b/.opencode/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.opencode/skills/impeccable/scripts/live-browser.js +++ b/.opencode/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.opencode/skills/impeccable/scripts/live/browser-script-parts.mjs b/.opencode/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.opencode/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.opencode/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.opencode/skills/impeccable/scripts/live/ui-surfaces.mjs b/.opencode/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.opencode/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.pi/skills/impeccable/reference/bolder.md b/.pi/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.pi/skills/impeccable/reference/bolder.md +++ b/.pi/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.pi/skills/impeccable/reference/new-work.md b/.pi/skills/impeccable/reference/new-work.md index 57437e7ba..ffc87a930 100644 --- a/.pi/skills/impeccable/reference/new-work.md +++ b/.pi/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .pi/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .pi/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .pi/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .pi/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.pi/skills/impeccable/reference/visualize.md b/.pi/skills/impeccable/reference/visualize.md index 0608624d9..539dd0286 100644 --- a/.pi/skills/impeccable/reference/visualize.md +++ b/.pi/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. 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. 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. diff --git a/.pi/skills/impeccable/scripts/concept-seed.mjs b/.pi/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.pi/skills/impeccable/scripts/concept-seed.mjs +++ b/.pi/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only 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, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${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. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - 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}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${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. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a 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. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; 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) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}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}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.pi/skills/impeccable/scripts/lib/impeccable-config.mjs b/.pi/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.pi/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.pi/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.pi/skills/impeccable/scripts/live-browser.js b/.pi/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.pi/skills/impeccable/scripts/live-browser.js +++ b/.pi/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.pi/skills/impeccable/scripts/live/browser-script-parts.mjs b/.pi/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.pi/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.pi/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.pi/skills/impeccable/scripts/live/ui-surfaces.mjs b/.pi/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.pi/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.qoder/skills/impeccable/reference/bolder.md b/.qoder/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.qoder/skills/impeccable/reference/bolder.md +++ b/.qoder/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.qoder/skills/impeccable/reference/new-work.md b/.qoder/skills/impeccable/reference/new-work.md index 983ce2b96..c0ce92c06 100644 --- a/.qoder/skills/impeccable/reference/new-work.md +++ b/.qoder/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .qoder/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .qoder/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .qoder/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .qoder/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.qoder/skills/impeccable/reference/visualize.md b/.qoder/skills/impeccable/reference/visualize.md index 7ccc0af4c..3e6a3a7e9 100644 --- a/.qoder/skills/impeccable/reference/visualize.md +++ b/.qoder/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. 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. 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. diff --git a/.qoder/skills/impeccable/scripts/concept-seed.mjs b/.qoder/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.qoder/skills/impeccable/scripts/concept-seed.mjs +++ b/.qoder/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only 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, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${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. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - 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}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${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. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a 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. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; 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) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}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}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.qoder/skills/impeccable/scripts/lib/impeccable-config.mjs b/.qoder/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.qoder/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.qoder/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.qoder/skills/impeccable/scripts/live-browser.js b/.qoder/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.qoder/skills/impeccable/scripts/live-browser.js +++ b/.qoder/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.qoder/skills/impeccable/scripts/live/browser-script-parts.mjs b/.qoder/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.qoder/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.qoder/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.qoder/skills/impeccable/scripts/live/ui-surfaces.mjs b/.qoder/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.qoder/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.rovodev/skills/impeccable/reference/bolder.md b/.rovodev/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.rovodev/skills/impeccable/reference/bolder.md +++ b/.rovodev/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.rovodev/skills/impeccable/reference/new-work.md b/.rovodev/skills/impeccable/reference/new-work.md index 9b3edee90..c4a7821fc 100644 --- a/.rovodev/skills/impeccable/reference/new-work.md +++ b/.rovodev/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .rovodev/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .rovodev/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .rovodev/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .rovodev/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.rovodev/skills/impeccable/reference/visualize.md b/.rovodev/skills/impeccable/reference/visualize.md index ad1f4a548..665e8c71c 100644 --- a/.rovodev/skills/impeccable/reference/visualize.md +++ b/.rovodev/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. 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. 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. diff --git a/.rovodev/skills/impeccable/scripts/concept-seed.mjs b/.rovodev/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.rovodev/skills/impeccable/scripts/concept-seed.mjs +++ b/.rovodev/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only 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, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${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. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - 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}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${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. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a 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. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; 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) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}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}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.rovodev/skills/impeccable/scripts/lib/impeccable-config.mjs b/.rovodev/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.rovodev/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.rovodev/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.rovodev/skills/impeccable/scripts/live-browser.js b/.rovodev/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.rovodev/skills/impeccable/scripts/live-browser.js +++ b/.rovodev/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.rovodev/skills/impeccable/scripts/live/browser-script-parts.mjs b/.rovodev/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.rovodev/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.rovodev/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.rovodev/skills/impeccable/scripts/live/ui-surfaces.mjs b/.rovodev/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.rovodev/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.trae-cn/skills/impeccable/reference/bolder.md b/.trae-cn/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.trae-cn/skills/impeccable/reference/bolder.md +++ b/.trae-cn/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.trae-cn/skills/impeccable/reference/new-work.md b/.trae-cn/skills/impeccable/reference/new-work.md index f47a72b20..529b63dbc 100644 --- a/.trae-cn/skills/impeccable/reference/new-work.md +++ b/.trae-cn/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .trae-cn/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .trae-cn/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .trae-cn/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .trae-cn/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.trae-cn/skills/impeccable/reference/visualize.md b/.trae-cn/skills/impeccable/reference/visualize.md index 0780bbf8b..6d39ce00a 100644 --- a/.trae-cn/skills/impeccable/reference/visualize.md +++ b/.trae-cn/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. 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. 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. diff --git a/.trae-cn/skills/impeccable/scripts/concept-seed.mjs b/.trae-cn/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.trae-cn/skills/impeccable/scripts/concept-seed.mjs +++ b/.trae-cn/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only 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, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${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. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - 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}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${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. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a 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. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; 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) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}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}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.trae-cn/skills/impeccable/scripts/lib/impeccable-config.mjs b/.trae-cn/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.trae-cn/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.trae-cn/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.trae-cn/skills/impeccable/scripts/live-browser.js b/.trae-cn/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.trae-cn/skills/impeccable/scripts/live-browser.js +++ b/.trae-cn/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.trae-cn/skills/impeccable/scripts/live/browser-script-parts.mjs b/.trae-cn/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.trae-cn/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.trae-cn/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.trae-cn/skills/impeccable/scripts/live/ui-surfaces.mjs b/.trae-cn/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.trae-cn/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.trae/skills/impeccable/reference/bolder.md b/.trae/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.trae/skills/impeccable/reference/bolder.md +++ b/.trae/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.trae/skills/impeccable/reference/new-work.md b/.trae/skills/impeccable/reference/new-work.md index f70c82584..5c805afc8 100644 --- a/.trae/skills/impeccable/reference/new-work.md +++ b/.trae/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .trae/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .trae/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .trae/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .trae/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.trae/skills/impeccable/reference/visualize.md b/.trae/skills/impeccable/reference/visualize.md index ef675864c..247ea2067 100644 --- a/.trae/skills/impeccable/reference/visualize.md +++ b/.trae/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. 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. 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. diff --git a/.trae/skills/impeccable/scripts/concept-seed.mjs b/.trae/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.trae/skills/impeccable/scripts/concept-seed.mjs +++ b/.trae/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only 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, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${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. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - 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}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${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. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a 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. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; 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) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}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}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.trae/skills/impeccable/scripts/lib/impeccable-config.mjs b/.trae/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.trae/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.trae/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.trae/skills/impeccable/scripts/live-browser.js b/.trae/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.trae/skills/impeccable/scripts/live-browser.js +++ b/.trae/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.trae/skills/impeccable/scripts/live/browser-script-parts.mjs b/.trae/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.trae/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.trae/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.trae/skills/impeccable/scripts/live/ui-surfaces.mjs b/.trae/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.trae/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.vibe/skills/impeccable/reference/bolder.md b/.vibe/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.vibe/skills/impeccable/reference/bolder.md +++ b/.vibe/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.vibe/skills/impeccable/reference/new-work.md b/.vibe/skills/impeccable/reference/new-work.md index bda245059..4adf881d6 100644 --- a/.vibe/skills/impeccable/reference/new-work.md +++ b/.vibe/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .vibe/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .vibe/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .vibe/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .vibe/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.vibe/skills/impeccable/reference/visualize.md b/.vibe/skills/impeccable/reference/visualize.md index 87e410295..a8bed5230 100644 --- a/.vibe/skills/impeccable/reference/visualize.md +++ b/.vibe/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. 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. 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. diff --git a/.vibe/skills/impeccable/scripts/concept-seed.mjs b/.vibe/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.vibe/skills/impeccable/scripts/concept-seed.mjs +++ b/.vibe/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only 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, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${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. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - 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}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${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. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a 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. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; 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) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}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}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.vibe/skills/impeccable/scripts/lib/impeccable-config.mjs b/.vibe/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.vibe/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.vibe/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.vibe/skills/impeccable/scripts/live-browser.js b/.vibe/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.vibe/skills/impeccable/scripts/live-browser.js +++ b/.vibe/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.vibe/skills/impeccable/scripts/live/browser-script-parts.mjs b/.vibe/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.vibe/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.vibe/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.vibe/skills/impeccable/scripts/live/ui-surfaces.mjs b/.vibe/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.vibe/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/plugin/skills/impeccable/reference/bolder.md b/plugin/skills/impeccable/reference/bolder.md index fced49456..a5c34cd3e 100644 --- a/plugin/skills/impeccable/reference/bolder.md +++ b/plugin/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/plugin/skills/impeccable/reference/new-work.md b/plugin/skills/impeccable/reference/new-work.md index 5161d25cb..718b4bd0b 100644 --- a/plugin/skills/impeccable/reference/new-work.md +++ b/plugin/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .claude/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .claude/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .claude/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .claude/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -80,7 +82,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/plugin/skills/impeccable/reference/visualize.md b/plugin/skills/impeccable/reference/visualize.md index 94c337f15..6df08b30b 100644 --- a/plugin/skills/impeccable/reference/visualize.md +++ b/plugin/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. 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. 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. diff --git a/plugin/skills/impeccable/scripts/concept-seed.mjs b/plugin/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/plugin/skills/impeccable/scripts/concept-seed.mjs +++ b/plugin/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only 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, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${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. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - 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}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${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. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a 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. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; 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) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}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}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/plugin/skills/impeccable/scripts/lib/impeccable-config.mjs b/plugin/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/plugin/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/plugin/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/plugin/skills/impeccable/scripts/live-browser.js b/plugin/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/plugin/skills/impeccable/scripts/live-browser.js +++ b/plugin/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/plugin/skills/impeccable/scripts/live/browser-script-parts.mjs b/plugin/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/plugin/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/plugin/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/plugin/skills/impeccable/scripts/live/ui-surfaces.mjs b/plugin/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/plugin/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } From dbff0880e62a0c05efbc099370a5c89384c0eb9b Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Sat, 8 Aug 2026 15:47:11 -0700 Subject: [PATCH 03/29] Decision page: full-fidelity comps, raise cycler, declined sizing, canon order, full card anatomy (#545) * Polish the decision page: raise cycler, declined height, canon order, full card anatomy Field feedback from the first real rolls of the verdict-routed hand: - Several raises stacked on the assigned card blew it out of proportion. More than one raise now renders as a compact cycler: one visible, a counter, click or Enter advances. A single raise stays inline. - Declined cards inherited the row's stretch alignment, so a narrow card stood at the tallest contender's height, a strange stilt beside the hand. They now size to their content. - Deck order becomes a gradient of standing: contenders, then the canon, then declined dead last. The canon between full alternates and the demoted row reads as the familiar door rather than the last resort after the rejects. - Root cause of bare-bones challenger and canon cards in the field: the --schema example only gave the assigned card palette, materials, and risk, and models author payloads by imitating the example, so the "same anatomy on every card" instruction lost to it every time. The example now carries full anatomy on every card and the schema note says a card with no palette chips is an authoring gap, not a data gap. AI-assisted change. Co-Authored-By: Claude Code * Decision cards carry full-fidelity comps instead of sketches Field verdict on the sketch contract: the sketches came back too simple to inform the choice, and generation takes the same time at any fidelity, so the deliberately-unfinished frame paid comp cost for sketch quality. The decision card's image is now that direction's north-star comp, produced under visualize.md's comp discipline (structure-led prompt, real name and content, no invented commercial claims), saved under .impeccable/mocks/ with its prompt sidecar. Fairness between cards comes from equal fidelity in each card's own grammar rather than shared unfinishedness. The chosen card's comp is never spent by the choice: on a comp-led build it enters the comp round as compositional option one (visualize.md now generates two variations beside it; a round arriving with no decision comp still renders all three), and on a code-led build it returns at the finish review as the critique reference. Produce order still front-loads a re-roll's spend onto the cards read first. serve-question keeps the sketch field's wire name for payload compatibility; docs, schema paths, shimmer labels, and the answer directive (CHOSEN COMP) speak comp. AI-assisted change. Co-Authored-By: Claude Code * fix: address PR review bot findings on the comp round - Producer still forced sketches (cursor, high): the asset producer's Decision Sketches contract still mandated deliberately unfinished matte sketches, so the parallel path would keep shipping sketch-era images. The section is now Decision Comps: full-fidelity north-star comp, structure-led prompt, equal commitment across siblings, no invented claims, sidecar written. - Mocks collided with the approval check (cursor, high): decision comps now live under .impeccable/mocks/decision/, visualize.md scopes the no-approval finding to comp-round output, new-work.md states the unchosen hand implies no approval, and the code-led finish packet names the chosen decision comp as the critique reference in the approved-comp slot. - Raise cycler announces (greptile, both P1s): a visually hidden aria-live region reads out the newly active raise and its position on advance; initial render stays quiet. - Declined width in the vertical deck (cursor, medium): align-self: flex-start shrank declined cards to content width in the portrait column layout, where the cross axis is horizontal; they stretch there and keep content height in the row layout. AI-assisted change. Co-Authored-By: Claude Code * fix: raise cycler tooltip and label name both input modes Copilot: the tooltip said Click while the control also answers Enter and Space; the title and a new aria-label now say activate/press Enter. AI-assisted change. Co-Authored-By: Claude Code * fix: finish reviewer exempts decision comps from the approval check cursor[bot] follow-through: the reviewer's Persistence check still treated any comps under .impeccable/mocks/ as approval-gated, and the reviewer never reads visualize.md by design, so code-led and spent-hand rounds could draw a false skipped-approval finding. The check now scopes to comp-round comps, exempts .impeccable/mocks/decision/ as the direction round's dealt hand, and defines how a code-led build's decision comp is judged in the approved-comp slot: the critique reference, under the no-approved-comp fidelity rules plus what the image dared that the build did not. AI-assisted change. Co-Authored-By: Claude Code * fix: the critique reference is its own reviewer input, not the approved-comp slot cursor[bot]: passing the code-led decision comp through the approved-comp slot dragged in that slot's obligations (inventory-first reading, the fidelity matrix, Truth's shipped-asset demand for every image-native region), which contradicts code-led's premise. The input contract now names it a separate labeled critique-reference input that nothing binding "the approved comp" touches, and Fidelity defines its treatment where the no-approved-comp rules live: provocation, not spec; no matrix, citations, or asset obligations; its dares enter material_fixes as ordinary fixes. AI-assisted change. Co-Authored-By: Claude Code --------- Co-authored-by: Claude Code --- skill/agents/impeccable-asset-producer.md | 4 +- skill/agents/impeccable-finish-reviewer.md | 6 +- skill/reference/new-work.md | 6 +- skill/reference/visualize.md | 4 +- skill/scripts/serve-question.mjs | 96 +++++++++++++++++----- tests/new-work-e2e.test.mjs | 4 +- tests/serve-question.test.mjs | 2 +- 7 files changed, 89 insertions(+), 33 deletions(-) diff --git a/skill/agents/impeccable-asset-producer.md b/skill/agents/impeccable-asset-producer.md index 0808db3b1..6af0ab8ea 100644 --- a/skill/agents/impeccable-asset-producer.md +++ b/skill/agents/impeccable-asset-producer.md @@ -22,9 +22,9 @@ Your job is production cleanup, not new art direction. Work only from the approv Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster. -## Decision Sketches +## Decision Comps -When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame, including its aspect: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. The frame is shared across siblings so no sketch looks more finished than another; a finish gap breaks the comparison. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a sketch run. +When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `sketch` path (the field keeps its wire name) the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a comp is reported back, not padded from imagination. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (its regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment is what keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run. ## Input Contract diff --git a/skill/agents/impeccable-finish-reviewer.md b/skill/agents/impeccable-finish-reviewer.md index 3964c09e3..588c1545e 100644 --- a/skill/agents/impeccable-finish-reviewer.md +++ b/skill/agents/impeccable-finish-reviewer.md @@ -22,12 +22,12 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path; and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order -1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comps with no recorded pick mean the approval point was skipped, and that is a material finding. -2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. +1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comp-round comps with no recorded pick mean the approval point was skipped, and that is a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and they imply no approval whatever the build path; a code-led build has no comp round at all. +2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. A critique-reference comp, when one arrived on such a build, is provocation rather than spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is the question of what the image dared that the build did not, and the dares worth adopting enter material_fixes as ordinary ordered fixes. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. 3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition. 4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport. 5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material. diff --git a/skill/reference/new-work.md b/skill/reference/new-work.md index c39e84810..adddcc12e 100644 --- a/skill/reference/new-work.md +++ b/skill/reference/new-work.md @@ -48,7 +48,7 @@ The script assigns which structure gets built; your top-ranked structure is what The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node {{scripts_path}}/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. +When image generation exists, every card also declares a `sketch` path under `.impeccable/mocks/decision/` (the field keeps its wire name for compatibility; what it carries is the card's comp), the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the comps; the page shimmer-waits per slot and the user may answer before they land. Each card's image is that direction's north-star comp at full fidelity, produced under the comp discipline in [visualize.md](visualize.md): the requested surface's first viewport, structure-led prompt, real product name and real content, no invented commercial claims, in that card's own palette, type character, and material world, committed all the way. Generation takes the same time at any fidelity, so an unfinished sketch pays sketch quality for comp cost; fairness between cards comes from equal fidelity in each card's own grammar, one surface, one aspect, never from shared unfinishedness. The frame's aspect is the surface's own: a native app or mobile-first surface comps portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen comped landscape is a broken frame, not a neutral default. Produce in the order the user reads, the assigned card, then the pick, then the full-card hand, then canon, each file written with its prompt sidecar the moment it is done, so a re-roll's spend front-loads onto the cards read first; declined challengers get no comp, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-comp packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. The chosen card's comp is not spent by the choice: on a comp-led build it enters the comp round as compositional option one, and on a code-led build it returns at the finish review as the critique reference, what the image dared that the build did not. The unchosen comps stay in `.impeccable/mocks/decision/` as the round's spent hand; they carry no approval and imply none. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. @@ -84,7 +84,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. @@ -109,6 +109,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node {{scripts_path}}/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths, and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node {{scripts_path}}/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/skill/reference/visualize.md b/skill/reference/visualize.md index d6e3764d8..09dc93182 100644 --- a/skill/reference/visualize.md +++ b/skill/reference/visualize.md @@ -6,7 +6,7 @@ The purpose of a probe is to test composition, narrative, hierarchy, density, fo ## 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. A decision-page sketch is not a probe: it chose the direction at deliberately unfinished fidelity, so the three comps render regardless, and the chosen card's sketch seeds at most one of them. +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. - 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. - When the user shortlisted multiple concepts, spread the three across them. @@ -22,7 +22,7 @@ Show the three together: in the harness when it can display images, otherwise on Do not begin code until the user approves a direction or explicitly delegates the choice. If they delegate, choose using the task brief, PRODUCT.md, and DESIGN.md, and state the evidence. Approval refines the task concept; it does not modify DESIGN.md. -This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build with generated comps and no recorded approval as carrying a material finding. +This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build whose comp round produced comps with no recorded approval as carrying a material finding; decision comps under `.impeccable/mocks/decision/` are the direction round's hand, not comp-round output, and imply no approval on their own. After approval, record the choice where tools can find it: the approved comp's path goes in the surface brief, and the approved comp's `.json` prompt sidecar gains `"approved": true` (every comp generated through `generate-image.mjs` has one; create it if a native tool didn't). The sidecar travels with the mocks folder, so the approval survives sessions and machines that never see the brief. Then summarize the composition and the parts of the comp that must not be literalized, return to new-work.md, record the direction contract from the approved surface concept, and build. diff --git a/skill/scripts/serve-question.mjs b/skill/scripts/serve-question.mjs index bbbbdf77e..788211920 100644 --- a/skill/scripts/serve-question.mjs +++ b/skill/scripts/serve-question.mjs @@ -42,10 +42,12 @@ * // raise lines under the identity row * "risk": "one line: the honest risk", // optional * "body": "fallback prose when the structured fields are absent", - * "sketch": ".impeccable/sketches/assigned.webp", // optional; may not exist - * // yet: the page shimmer-waits and polls the - * // slot until the file lands, so serve first - * // and generate after + * "sketch": ".impeccable/mocks/decision/assigned.webp", // optional; the card's + * // full-fidelity direction comp (the field + * // keeps the sketch era's wire name). May not + * // exist yet: the page shimmer-waits and + * // polls the slot until the file lands, so + * // serve first and generate after * "hero": "https://... or /abs/path.webp", // optional inspiration image; * // rides picture-in-picture when a sketch exists * "board": "https://... or /abs/path.webp" // optional secondary image @@ -147,7 +149,7 @@ function printAnswer(raw) { console.log("CHOSEN CARD: open the chosen world's board and hero images now, before any code. When your harness only reads files, or runs sandboxed, download them INTO the workspace and open the relative path; a sandboxed viewer rejects absolute paths outside it. They set the craft bar the build must reach."); } if (a.sketch) { - console.log('CHOSEN SKETCH: the decision sketch at that path may seed one comp probe; the comp round still renders its full set, because a sketch chose the direction, not the composition.'); + console.log('CHOSEN COMP: the decision comp at that path is compositional option one. On a comp-led build the comp round adds two variations beside it; on a code-led build it returns at the finish review as the critique reference. Never regenerate it from scratch.'); } if (a.optionId === 'canon') { console.log('CANON CHOSEN: the user picked the category standard on purpose. Ask once for two or three products this should sit alongside; their craft level becomes the quality bar. Execute the canon at full commitment, conventions embraced without irony or smuggled quirk.'); @@ -173,17 +175,17 @@ if (hasFlag('schema')) { title: 'Choose the visual world', question: 'The roll assigned Fillmore Handbill. Keep it, take an alternate, or re-roll.', options: [ - { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, - { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/sketches/model-pick.webp' }, - { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, - { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, + { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/mocks/decision/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, + { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', palette: ['#1f1c18', '#efe5d0', '#a33327'], materials: ['woodcut', 'rag paper'], viewport: 'One tall sheet, the newest release as today’s ballad.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/mocks/decision/model-pick.webp' }, + { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', palette: ['#0000c0', '#ffff00', '#00c000', '#ffffff'], materials: ['block mosaic', 'phosphor glow'], viewport: 'P100 index page, releases as numbered rows.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', risk: 'Reads retro-novelty when the grid is not strict.', sketch: '.impeccable/mocks/decision/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, + { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', palette: ['#101418', '#9fb4c0'], materials: ['film grain', 'backlit glass'], case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, ], reroll: { registers: ['safer', 'bolder'] }, canon: true, - canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', viewport: 'The arrangement a visitor expects, at full craft.', sketch: '.impeccable/sketches/canon.webp' }, + 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.', sketch: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. 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. Sketch 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/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY 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 sketch slot carries the card’s full-fidelity direction comp (the field keeps its wire name for compatibility). 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); } @@ -337,16 +339,19 @@ function loadRound(json) { sketchSrc: sketchSrc(option.sketch), }); options = parsed.options.map(decorate); - // The verdict routes rendering: full cards first, declined cards demoted to - // the end of the deck in their own payload order. The reorder happens here - // so a payload that interleaves them still renders the weighing's shape. + // The verdict routes rendering: full cards first, then the canon, then the + // declined cards dead last in their own payload order. The reorder happens + // here so a payload that interleaves them still renders the weighing's + // shape, and the deck reads as a gradient of standing: contenders, the + // familiar door, then the demoted row. const declined = options.filter((o) => o.verdict === 'declined'); - if (declined.length) options = [...options.filter((o) => o.verdict !== 'declined'), ...declined]; + options = options.filter((o) => o.verdict !== 'declined'); // The standing exit as a full card: same anatomy, reserved id, rendered // subordinate by the page. Without it, canon stays the quiet footer action. if (parsed.canonCard && typeof parsed.canonCard === 'object') { options = [...options, { ...decorate(parsed.canonCard), id: 'canon', isCanon: true }]; } + options = [...options, ...declined]; } try { loadRound(raw); } catch (error) { console.error(`serve-question: ${error.message}`); process.exit(1); } const detachedKey = hasFlag('detached-serve') ? arg('key') : null; @@ -392,10 +397,21 @@ function page() { if (idBits.length) rows.push(`
${idBits.join('')}
`); // Donations from declined challengers render as named raise lines: the // assigned card arrives already raised by the hand it beat, and the raise - // is readable, because a raise nobody can read did not happen. + // is readable, because a raise nobody can read did not happen. One raise + // renders inline; several become a compact cycler (click advances), so a + // generous hand cannot blow the card out of proportion. if (Array.isArray(option.raised) && option.raised.length) { const nameOf = (id) => options.find((o) => o.id === id)?.label || String(id ?? ''); - rows.push(`
${option.raised.slice(0, 4).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`).join('')}
`); + const raiseLines = option.raised.slice(0, 6).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`); + if (raiseLines.length > 1) { + rows.push(`
+
Raised by the hand1/${raiseLines.length}
+ ${raiseLines.join('')} + +
`); + } else { + rows.push(`
${raiseLines[0]}
`); + } } // Demoted art stays reachable as a labeled thumb: the catalog world // explains where the direction comes from without buying it back the @@ -438,7 +454,7 @@ function page() { if (thumbOnly(option)) return ''; if (faceSketch(option)) { return `
-
sketching…
+
rendering…
${inspiration}
${expandChip}${details}
@@ -568,6 +584,10 @@ function page() { .nav.next { right: auto; left: 50%; top: auto; bottom: 6px; transform: translate(-50%, 0); } .fade-prev { top: 0; left: 0; right: 0; bottom: auto; width: auto; height: 72px; background: linear-gradient(180deg, var(--ks-lacquer), transparent); } .fade-next { top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 72px; background: linear-gradient(0deg, var(--ks-lacquer), transparent); } + /* In the vertical deck the cross axis is horizontal: flex-start would + shrink a declined card to content WIDTH, not height, so it stretches + like every other card and its height is already its own. */ + .grid > .card.declined { align-self: stretch; } } .card { position: relative; perspective: 1400px; transform: rotate(var(--fan, 0deg)); transition: transform .25s cubic-bezier(.16, 1, .3, 1); } .card:hover { transform: rotate(0deg) translateY(-4px); } @@ -660,7 +680,10 @@ function page() { and quieter, its catalog art rides as a labeled thumb in the body, and the action reads "Adopt anyway". Adoptable, never deleted: the demoted row is the hand's proof of judgment. */ - .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); } + /* Narrow AND short: without align-self the stretch default drags a thin + declined card to the tallest contender's height, a strange stilt of a + card beside the full hand. */ + .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); align-self: flex-start; } .card.declined .face { background: var(--ks-graphite); } .card.declined:hover .face { border-color: var(--ks-text-faint); } .card.declined h2 { font-size: 1rem; color: var(--ks-text); } @@ -677,6 +700,18 @@ function page() { .raises { display: flex; flex-direction: column; gap: 4px; margin: 2px 0; } .raise { font-size: .78rem; color: var(--ks-text-muted); line-height: 1.45; border-left: 2px solid var(--ks-patina); padding-left: 8px; } .raise .fact-label { color: var(--ks-patina); } + /* Several raises cycle instead of stacking: one visible at a time, a + counter for the rest, the whole block advances on click. */ + .raises-cycle { cursor: pointer; border-radius: 6px; } + .raises-cycle .raise { display: none; border-left: none; padding-left: 0; } + .raises-cycle .raise.active { display: block; } + .raises-cycle { border-left: 2px solid var(--ks-patina); padding-left: 8px; } + .raises-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; } + .raises-head .fact-label { color: var(--ks-patina); } + .raises-count { font-family: var(--ks-mono); font-size: .58rem; letter-spacing: .14em; color: var(--ks-text-faint); } + .raises-count::after { content: " \\203A"; } + .raises-cycle:hover .raises-count { color: var(--ks-patina); } + .sr-live { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } /* The standing exit as a card: present with full anatomy, never dressed as a contender. Graphite instead of kinpaku, and it never takes the lead ring. */ .card.canon .face { border-color: var(--ks-rule); background: var(--ks-graphite); } @@ -776,6 +811,25 @@ function page() { b.closest('.card').classList.toggle('flipped'); })); + // Raise cycler: click (or Enter) advances to the next donation. + document.querySelectorAll('.raises-cycle').forEach(cycle => { + const raises = [...cycle.querySelectorAll('.raise')]; + const count = cycle.querySelector('[data-raises-count]'); + let at = 0; + const live = cycle.querySelector('.sr-live'); + const show = (announce) => { + raises.forEach((raise, i) => raise.classList.toggle('active', i === at)); + if (count) count.textContent = (at + 1) + '/' + raises.length; + // Screen readers hear the raise they just advanced to; the initial + // render stays quiet so page load does not narrate every card. + if (announce && live) live.textContent = 'Raise ' + (at + 1) + ' of ' + raises.length + ': ' + (raises[at]?.textContent || ''); + }; + show(false); + const advance = (e) => { e.stopPropagation(); at = (at + 1) % raises.length; show(true); }; + cycle.addEventListener('click', advance); + cycle.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); advance(e); } }); + }); + // Deal from the stack: cards begin piled at the grid's center, blurred, // then travel to their seats with a stagger. const cards = [...document.querySelectorAll('.card')]; @@ -823,7 +877,7 @@ function page() { const note = m.querySelector('.sketch-note'); const started = Date.now(); // A live elapsed count is the difference between "working" and "frozen". - const tick = setInterval(() => { if (note) note.textContent = 'sketching · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); + const tick = setInterval(() => { if (note) note.textContent = 'rendering · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); const settle = () => { clearInterval(tick); m.classList.remove('sketching', 'stand-in'); m.querySelector('.shimmer')?.remove(); m.querySelector('.stand-in-label')?.remove(); }; const standIn = () => { const pip = m.querySelector('.pip img'); @@ -834,7 +888,7 @@ function page() { clearInterval(tick); const label = document.createElement('p'); label.className = 'stand-in-label'; - label.textContent = 'inspiration · sketch pending'; + label.textContent = 'inspiration · comp pending'; m.appendChild(label); }; const tryLoad = () => { diff --git a/tests/new-work-e2e.test.mjs b/tests/new-work-e2e.test.mjs index 067c2c002..4a3b546a5 100644 --- a/tests/new-work-e2e.test.mjs +++ b/tests/new-work-e2e.test.mjs @@ -340,6 +340,8 @@ describe('new-work-e2e: serve-question decision page', () => { { id: 'challenger-waxprint', label: 'Wax Print Market', verdict: 'competitive', hero: winnerHero }, ], reroll: true, steer: true, + canon: true, + canonCard: { label: 'The category standard', thesis: 'What the category ships.' }, }; const { url } = await startDaemon(cwd, payload, key); try { @@ -361,7 +363,7 @@ describe('new-work-e2e: serve-question decision page', () => { await context.close(); assert.equal(collected.code, 0, collected.out); const answer = JSON.parse(collected.out.match(/ANSWER: (\{.*\})/)[1]); - assert.deepEqual(order, ['assigned', 'challenger-waxprint', 'challenger-deepsea'], 'declined cards reorder to the end'); + assert.deepEqual(order, ['assigned', 'challenger-waxprint', 'canon', 'challenger-deepsea'], 'contenders, then the canon, then declined dead last'); assert.ok(declinedCard, 'declined verdict adds the .declined card class'); assert.equal(declinedMedia, null, 'declined catalog art never renders full-bleed'); assert.ok(declinedThumb, 'declined catalog art rides as a labeled thumb'); diff --git a/tests/serve-question.test.mjs b/tests/serve-question.test.mjs index f6dd97a53..2a8e22f2d 100644 --- a/tests/serve-question.test.mjs +++ b/tests/serve-question.test.mjs @@ -248,6 +248,6 @@ describe('serve-question', () => { const code = await new Promise((resolve) => child.on('exit', resolve)); assert.equal(code, 0); assert.match(read(), /"sketch":/); - assert.match(read(), /CHOSEN SKETCH:/); + assert.match(read(), /CHOSEN COMP:/); }); }); From f254e7685da47b07e15d66e405138537e7d77542 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 22:47:41 +0000 Subject: [PATCH 04/29] Sync generated provider output --- .../agents/impeccable_asset_producer.toml | 4 +- .../agents/impeccable_finish_reviewer.toml | 6 +- .../reference/degraded/asset-producer.md | 4 +- .../reference/degraded/finish-reviewer.md | 6 +- .../skills/impeccable/reference/new-work.md | 6 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/serve-question.mjs | 96 +++++++++++++++---- .claude/agents/impeccable-asset-producer.md | 4 +- .claude/agents/impeccable-finish-reviewer.md | 6 +- .../reference/degraded/asset-producer.md | 4 +- .../reference/degraded/finish-reviewer.md | 6 +- .../skills/impeccable/reference/new-work.md | 6 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/serve-question.mjs | 96 +++++++++++++++---- .cursor/agents/impeccable-asset-producer.md | 4 +- .cursor/agents/impeccable-finish-reviewer.md | 6 +- .../reference/degraded/asset-producer.md | 4 +- .../reference/degraded/finish-reviewer.md | 6 +- .../skills/impeccable/reference/new-work.md | 6 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/serve-question.mjs | 96 +++++++++++++++---- .../reference/degraded/asset-producer.md | 4 +- .../reference/degraded/finish-reviewer.md | 6 +- .../skills/impeccable/reference/new-work.md | 6 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/serve-question.mjs | 96 +++++++++++++++---- .../reference/degraded/asset-producer.md | 4 +- .../reference/degraded/finish-reviewer.md | 6 +- .../skills/impeccable/reference/new-work.md | 6 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/serve-question.mjs | 96 +++++++++++++++---- .grok/agents/impeccable-asset-producer.md | 4 +- .grok/agents/impeccable-finish-reviewer.md | 6 +- .../reference/degraded/asset-producer.md | 4 +- .../reference/degraded/finish-reviewer.md | 6 +- .grok/skills/impeccable/reference/new-work.md | 6 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/serve-question.mjs | 96 +++++++++++++++---- .../reference/degraded/asset-producer.md | 4 +- .../reference/degraded/finish-reviewer.md | 6 +- .kiro/skills/impeccable/reference/new-work.md | 6 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/serve-question.mjs | 96 +++++++++++++++---- .../reference/degraded/asset-producer.md | 4 +- .../reference/degraded/finish-reviewer.md | 6 +- .../skills/impeccable/reference/new-work.md | 6 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/serve-question.mjs | 96 +++++++++++++++---- .../reference/degraded/asset-producer.md | 4 +- .../reference/degraded/finish-reviewer.md | 6 +- .pi/skills/impeccable/reference/new-work.md | 6 +- .pi/skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/serve-question.mjs | 96 +++++++++++++++---- .../reference/degraded/asset-producer.md | 4 +- .../reference/degraded/finish-reviewer.md | 6 +- .../skills/impeccable/reference/new-work.md | 6 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/serve-question.mjs | 96 +++++++++++++++---- .../reference/degraded/asset-producer.md | 4 +- .../reference/degraded/finish-reviewer.md | 6 +- .../skills/impeccable/reference/new-work.md | 6 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/serve-question.mjs | 96 +++++++++++++++---- .../reference/degraded/asset-producer.md | 4 +- .../reference/degraded/finish-reviewer.md | 6 +- .../skills/impeccable/reference/new-work.md | 6 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/serve-question.mjs | 96 +++++++++++++++---- .../reference/degraded/asset-producer.md | 4 +- .../reference/degraded/finish-reviewer.md | 6 +- .trae/skills/impeccable/reference/new-work.md | 6 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/serve-question.mjs | 96 +++++++++++++++---- .../reference/degraded/asset-producer.md | 4 +- .../reference/degraded/finish-reviewer.md | 6 +- .vibe/skills/impeccable/reference/new-work.md | 6 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/serve-question.mjs | 96 +++++++++++++++---- plugin/agents/impeccable-asset-producer.md | 4 +- plugin/agents/impeccable-finish-reviewer.md | 6 +- .../reference/degraded/asset-producer.md | 4 +- .../reference/degraded/finish-reviewer.md | 6 +- .../skills/impeccable/reference/new-work.md | 6 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/serve-question.mjs | 96 +++++++++++++++---- 85 files changed, 1300 insertions(+), 490 deletions(-) diff --git a/.agents/skills/impeccable/agents/impeccable_asset_producer.toml b/.agents/skills/impeccable/agents/impeccable_asset_producer.toml index 073236d42..998cdd5f3 100644 --- a/.agents/skills/impeccable/agents/impeccable_asset_producer.toml +++ b/.agents/skills/impeccable/agents/impeccable_asset_producer.toml @@ -13,9 +13,9 @@ Your job is production cleanup, not new art direction. Work only from the approv Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster. -## Decision Sketches +## Decision Comps -When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame, including its aspect: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. The frame is shared across siblings so no sketch looks more finished than another; a finish gap breaks the comparison. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a sketch run. +When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `sketch` path (the field keeps its wire name) the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a comp is reported back, not padded from imagination. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (its regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment is what keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run. ## Input Contract diff --git a/.agents/skills/impeccable/agents/impeccable_finish_reviewer.toml b/.agents/skills/impeccable/agents/impeccable_finish_reviewer.toml index 232fdd5bd..8ca7e7e9f 100644 --- a/.agents/skills/impeccable/agents/impeccable_finish_reviewer.toml +++ b/.agents/skills/impeccable/agents/impeccable_finish_reviewer.toml @@ -13,12 +13,12 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path; and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order -1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comps with no recorded pick mean the approval point was skipped, and that is a material finding. -2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. +1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comp-round comps with no recorded pick mean the approval point was skipped, and that is a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and they imply no approval whatever the build path; a code-led build has no comp round at all. +2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. A critique-reference comp, when one arrived on such a build, is provocation rather than spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is the question of what the image dared that the build did not, and the dares worth adopting enter material_fixes as ordinary ordered fixes. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. 3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition. 4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport. 5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material. diff --git a/.agents/skills/impeccable/reference/degraded/asset-producer.md b/.agents/skills/impeccable/reference/degraded/asset-producer.md index fe9f7b183..fe12bb559 100644 --- a/.agents/skills/impeccable/reference/degraded/asset-producer.md +++ b/.agents/skills/impeccable/reference/degraded/asset-producer.md @@ -11,9 +11,9 @@ Your job is production cleanup, not new art direction. Work only from the approv Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster. -## Decision Sketches +## Decision Comps -When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame, including its aspect: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. The frame is shared across siblings so no sketch looks more finished than another; a finish gap breaks the comparison. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a sketch run. +When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `sketch` path (the field keeps its wire name) the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a comp is reported back, not padded from imagination. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (its regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment is what keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run. ## Input Contract diff --git a/.agents/skills/impeccable/reference/degraded/finish-reviewer.md b/.agents/skills/impeccable/reference/degraded/finish-reviewer.md index c49acadb0..c7bb7ca85 100644 --- a/.agents/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.agents/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,12 +11,12 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path; and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order -1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comps with no recorded pick mean the approval point was skipped, and that is a material finding. -2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. +1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comp-round comps with no recorded pick mean the approval point was skipped, and that is a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and they imply no approval whatever the build path; a code-led build has no comp round at all. +2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. A critique-reference comp, when one arrived on such a build, is provocation rather than spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is the question of what the image dared that the build did not, and the dares worth adopting enter material_fixes as ordinary ordered fixes. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. 3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition. 4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport. 5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material. diff --git a/.agents/skills/impeccable/reference/new-work.md b/.agents/skills/impeccable/reference/new-work.md index 8f654b7d7..51a12b51a 100644 --- a/.agents/skills/impeccable/reference/new-work.md +++ b/.agents/skills/impeccable/reference/new-work.md @@ -48,7 +48,7 @@ The script assigns which structure gets built; your top-ranked structure is what The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .agents/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. +When image generation exists, every card also declares a `sketch` path under `.impeccable/mocks/decision/` (the field keeps its wire name for compatibility; what it carries is the card's comp), the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the comps; the page shimmer-waits per slot and the user may answer before they land. Each card's image is that direction's north-star comp at full fidelity, produced under the comp discipline in [visualize.md](visualize.md): the requested surface's first viewport, structure-led prompt, real product name and real content, no invented commercial claims, in that card's own palette, type character, and material world, committed all the way. Generation takes the same time at any fidelity, so an unfinished sketch pays sketch quality for comp cost; fairness between cards comes from equal fidelity in each card's own grammar, one surface, one aspect, never from shared unfinishedness. The frame's aspect is the surface's own: a native app or mobile-first surface comps portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen comped landscape is a broken frame, not a neutral default. Produce in the order the user reads, the assigned card, then the pick, then the full-card hand, then canon, each file written with its prompt sidecar the moment it is done, so a re-roll's spend front-loads onto the cards read first; declined challengers get no comp, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-comp packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. The chosen card's comp is not spent by the choice: on a comp-led build it enters the comp round as compositional option one, and on a code-led build it returns at the finish review as the critique reference, what the image dared that the build did not. The unchosen comps stay in `.impeccable/mocks/decision/` as the round's spent hand; they carry no approval and imply none. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. @@ -80,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. @@ -105,6 +105,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .agents/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths, and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .agents/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.agents/skills/impeccable/reference/visualize.md b/.agents/skills/impeccable/reference/visualize.md index c20d910f3..d79c4e8b0 100644 --- a/.agents/skills/impeccable/reference/visualize.md +++ b/.agents/skills/impeccable/reference/visualize.md @@ -6,7 +6,7 @@ The purpose of a probe is to test composition, narrative, hierarchy, density, fo ## 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. A decision-page sketch is not a probe: it chose the direction at deliberately unfinished fidelity, so the three comps render regardless, and the chosen card's sketch seeds at most one of them. +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. - 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. - When the user shortlisted multiple concepts, spread the three across them. @@ -22,7 +22,7 @@ Show the three together: in the harness when it can display images, otherwise on Do not begin code until the user approves a direction or explicitly delegates the choice. If they delegate, choose using the task brief, PRODUCT.md, and DESIGN.md, and state the evidence. Approval refines the task concept; it does not modify DESIGN.md. -This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build with generated comps and no recorded approval as carrying a material finding. +This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build whose comp round produced comps with no recorded approval as carrying a material finding; decision comps under `.impeccable/mocks/decision/` are the direction round's hand, not comp-round output, and imply no approval on their own. After approval, record the choice where tools can find it: the approved comp's path goes in the surface brief, and the approved comp's `.json` prompt sidecar gains `"approved": true` (every comp generated through `generate-image.mjs` has one; create it if a native tool didn't). The sidecar travels with the mocks folder, so the approval survives sessions and machines that never see the brief. Then summarize the composition and the parts of the comp that must not be literalized, return to new-work.md, record the direction contract from the approved surface concept, and build. diff --git a/.agents/skills/impeccable/scripts/serve-question.mjs b/.agents/skills/impeccable/scripts/serve-question.mjs index bbbbdf77e..788211920 100644 --- a/.agents/skills/impeccable/scripts/serve-question.mjs +++ b/.agents/skills/impeccable/scripts/serve-question.mjs @@ -42,10 +42,12 @@ * // raise lines under the identity row * "risk": "one line: the honest risk", // optional * "body": "fallback prose when the structured fields are absent", - * "sketch": ".impeccable/sketches/assigned.webp", // optional; may not exist - * // yet: the page shimmer-waits and polls the - * // slot until the file lands, so serve first - * // and generate after + * "sketch": ".impeccable/mocks/decision/assigned.webp", // optional; the card's + * // full-fidelity direction comp (the field + * // keeps the sketch era's wire name). May not + * // exist yet: the page shimmer-waits and + * // polls the slot until the file lands, so + * // serve first and generate after * "hero": "https://... or /abs/path.webp", // optional inspiration image; * // rides picture-in-picture when a sketch exists * "board": "https://... or /abs/path.webp" // optional secondary image @@ -147,7 +149,7 @@ function printAnswer(raw) { console.log("CHOSEN CARD: open the chosen world's board and hero images now, before any code. When your harness only reads files, or runs sandboxed, download them INTO the workspace and open the relative path; a sandboxed viewer rejects absolute paths outside it. They set the craft bar the build must reach."); } if (a.sketch) { - console.log('CHOSEN SKETCH: the decision sketch at that path may seed one comp probe; the comp round still renders its full set, because a sketch chose the direction, not the composition.'); + console.log('CHOSEN COMP: the decision comp at that path is compositional option one. On a comp-led build the comp round adds two variations beside it; on a code-led build it returns at the finish review as the critique reference. Never regenerate it from scratch.'); } if (a.optionId === 'canon') { console.log('CANON CHOSEN: the user picked the category standard on purpose. Ask once for two or three products this should sit alongside; their craft level becomes the quality bar. Execute the canon at full commitment, conventions embraced without irony or smuggled quirk.'); @@ -173,17 +175,17 @@ if (hasFlag('schema')) { title: 'Choose the visual world', question: 'The roll assigned Fillmore Handbill. Keep it, take an alternate, or re-roll.', options: [ - { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, - { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/sketches/model-pick.webp' }, - { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, - { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, + { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/mocks/decision/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, + { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', palette: ['#1f1c18', '#efe5d0', '#a33327'], materials: ['woodcut', 'rag paper'], viewport: 'One tall sheet, the newest release as today’s ballad.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/mocks/decision/model-pick.webp' }, + { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', palette: ['#0000c0', '#ffff00', '#00c000', '#ffffff'], materials: ['block mosaic', 'phosphor glow'], viewport: 'P100 index page, releases as numbered rows.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', risk: 'Reads retro-novelty when the grid is not strict.', sketch: '.impeccable/mocks/decision/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, + { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', palette: ['#101418', '#9fb4c0'], materials: ['film grain', 'backlit glass'], case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, ], reroll: { registers: ['safer', 'bolder'] }, canon: true, - canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', viewport: 'The arrangement a visitor expects, at full craft.', sketch: '.impeccable/sketches/canon.webp' }, + 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.', sketch: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. 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. Sketch 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/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY 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 sketch slot carries the card’s full-fidelity direction comp (the field keeps its wire name for compatibility). 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); } @@ -337,16 +339,19 @@ function loadRound(json) { sketchSrc: sketchSrc(option.sketch), }); options = parsed.options.map(decorate); - // The verdict routes rendering: full cards first, declined cards demoted to - // the end of the deck in their own payload order. The reorder happens here - // so a payload that interleaves them still renders the weighing's shape. + // The verdict routes rendering: full cards first, then the canon, then the + // declined cards dead last in their own payload order. The reorder happens + // here so a payload that interleaves them still renders the weighing's + // shape, and the deck reads as a gradient of standing: contenders, the + // familiar door, then the demoted row. const declined = options.filter((o) => o.verdict === 'declined'); - if (declined.length) options = [...options.filter((o) => o.verdict !== 'declined'), ...declined]; + options = options.filter((o) => o.verdict !== 'declined'); // The standing exit as a full card: same anatomy, reserved id, rendered // subordinate by the page. Without it, canon stays the quiet footer action. if (parsed.canonCard && typeof parsed.canonCard === 'object') { options = [...options, { ...decorate(parsed.canonCard), id: 'canon', isCanon: true }]; } + options = [...options, ...declined]; } try { loadRound(raw); } catch (error) { console.error(`serve-question: ${error.message}`); process.exit(1); } const detachedKey = hasFlag('detached-serve') ? arg('key') : null; @@ -392,10 +397,21 @@ function page() { if (idBits.length) rows.push(`
${idBits.join('')}
`); // Donations from declined challengers render as named raise lines: the // assigned card arrives already raised by the hand it beat, and the raise - // is readable, because a raise nobody can read did not happen. + // is readable, because a raise nobody can read did not happen. One raise + // renders inline; several become a compact cycler (click advances), so a + // generous hand cannot blow the card out of proportion. if (Array.isArray(option.raised) && option.raised.length) { const nameOf = (id) => options.find((o) => o.id === id)?.label || String(id ?? ''); - rows.push(`
${option.raised.slice(0, 4).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`).join('')}
`); + const raiseLines = option.raised.slice(0, 6).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`); + if (raiseLines.length > 1) { + rows.push(`
+
Raised by the hand1/${raiseLines.length}
+ ${raiseLines.join('')} + +
`); + } else { + rows.push(`
${raiseLines[0]}
`); + } } // Demoted art stays reachable as a labeled thumb: the catalog world // explains where the direction comes from without buying it back the @@ -438,7 +454,7 @@ function page() { if (thumbOnly(option)) return ''; if (faceSketch(option)) { return `
-
sketching…
+
rendering…
${inspiration}
${expandChip}${details}
@@ -568,6 +584,10 @@ function page() { .nav.next { right: auto; left: 50%; top: auto; bottom: 6px; transform: translate(-50%, 0); } .fade-prev { top: 0; left: 0; right: 0; bottom: auto; width: auto; height: 72px; background: linear-gradient(180deg, var(--ks-lacquer), transparent); } .fade-next { top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 72px; background: linear-gradient(0deg, var(--ks-lacquer), transparent); } + /* In the vertical deck the cross axis is horizontal: flex-start would + shrink a declined card to content WIDTH, not height, so it stretches + like every other card and its height is already its own. */ + .grid > .card.declined { align-self: stretch; } } .card { position: relative; perspective: 1400px; transform: rotate(var(--fan, 0deg)); transition: transform .25s cubic-bezier(.16, 1, .3, 1); } .card:hover { transform: rotate(0deg) translateY(-4px); } @@ -660,7 +680,10 @@ function page() { and quieter, its catalog art rides as a labeled thumb in the body, and the action reads "Adopt anyway". Adoptable, never deleted: the demoted row is the hand's proof of judgment. */ - .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); } + /* Narrow AND short: without align-self the stretch default drags a thin + declined card to the tallest contender's height, a strange stilt of a + card beside the full hand. */ + .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); align-self: flex-start; } .card.declined .face { background: var(--ks-graphite); } .card.declined:hover .face { border-color: var(--ks-text-faint); } .card.declined h2 { font-size: 1rem; color: var(--ks-text); } @@ -677,6 +700,18 @@ function page() { .raises { display: flex; flex-direction: column; gap: 4px; margin: 2px 0; } .raise { font-size: .78rem; color: var(--ks-text-muted); line-height: 1.45; border-left: 2px solid var(--ks-patina); padding-left: 8px; } .raise .fact-label { color: var(--ks-patina); } + /* Several raises cycle instead of stacking: one visible at a time, a + counter for the rest, the whole block advances on click. */ + .raises-cycle { cursor: pointer; border-radius: 6px; } + .raises-cycle .raise { display: none; border-left: none; padding-left: 0; } + .raises-cycle .raise.active { display: block; } + .raises-cycle { border-left: 2px solid var(--ks-patina); padding-left: 8px; } + .raises-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; } + .raises-head .fact-label { color: var(--ks-patina); } + .raises-count { font-family: var(--ks-mono); font-size: .58rem; letter-spacing: .14em; color: var(--ks-text-faint); } + .raises-count::after { content: " \\203A"; } + .raises-cycle:hover .raises-count { color: var(--ks-patina); } + .sr-live { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } /* The standing exit as a card: present with full anatomy, never dressed as a contender. Graphite instead of kinpaku, and it never takes the lead ring. */ .card.canon .face { border-color: var(--ks-rule); background: var(--ks-graphite); } @@ -776,6 +811,25 @@ function page() { b.closest('.card').classList.toggle('flipped'); })); + // Raise cycler: click (or Enter) advances to the next donation. + document.querySelectorAll('.raises-cycle').forEach(cycle => { + const raises = [...cycle.querySelectorAll('.raise')]; + const count = cycle.querySelector('[data-raises-count]'); + let at = 0; + const live = cycle.querySelector('.sr-live'); + const show = (announce) => { + raises.forEach((raise, i) => raise.classList.toggle('active', i === at)); + if (count) count.textContent = (at + 1) + '/' + raises.length; + // Screen readers hear the raise they just advanced to; the initial + // render stays quiet so page load does not narrate every card. + if (announce && live) live.textContent = 'Raise ' + (at + 1) + ' of ' + raises.length + ': ' + (raises[at]?.textContent || ''); + }; + show(false); + const advance = (e) => { e.stopPropagation(); at = (at + 1) % raises.length; show(true); }; + cycle.addEventListener('click', advance); + cycle.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); advance(e); } }); + }); + // Deal from the stack: cards begin piled at the grid's center, blurred, // then travel to their seats with a stagger. const cards = [...document.querySelectorAll('.card')]; @@ -823,7 +877,7 @@ function page() { const note = m.querySelector('.sketch-note'); const started = Date.now(); // A live elapsed count is the difference between "working" and "frozen". - const tick = setInterval(() => { if (note) note.textContent = 'sketching · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); + const tick = setInterval(() => { if (note) note.textContent = 'rendering · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); const settle = () => { clearInterval(tick); m.classList.remove('sketching', 'stand-in'); m.querySelector('.shimmer')?.remove(); m.querySelector('.stand-in-label')?.remove(); }; const standIn = () => { const pip = m.querySelector('.pip img'); @@ -834,7 +888,7 @@ function page() { clearInterval(tick); const label = document.createElement('p'); label.className = 'stand-in-label'; - label.textContent = 'inspiration · sketch pending'; + label.textContent = 'inspiration · comp pending'; m.appendChild(label); }; const tryLoad = () => { diff --git a/.claude/agents/impeccable-asset-producer.md b/.claude/agents/impeccable-asset-producer.md index 2ee6a9439..600f0f8f4 100644 --- a/.claude/agents/impeccable-asset-producer.md +++ b/.claude/agents/impeccable-asset-producer.md @@ -16,9 +16,9 @@ Your job is production cleanup, not new art direction. Work only from the approv Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster. -## Decision Sketches +## Decision Comps -When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame, including its aspect: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. The frame is shared across siblings so no sketch looks more finished than another; a finish gap breaks the comparison. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a sketch run. +When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `sketch` path (the field keeps its wire name) the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a comp is reported back, not padded from imagination. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (its regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment is what keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run. ## Input Contract diff --git a/.claude/agents/impeccable-finish-reviewer.md b/.claude/agents/impeccable-finish-reviewer.md index 7c71679c1..02658bf8e 100644 --- a/.claude/agents/impeccable-finish-reviewer.md +++ b/.claude/agents/impeccable-finish-reviewer.md @@ -16,12 +16,12 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path; and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order -1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comps with no recorded pick mean the approval point was skipped, and that is a material finding. -2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. +1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comp-round comps with no recorded pick mean the approval point was skipped, and that is a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and they imply no approval whatever the build path; a code-led build has no comp round at all. +2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. A critique-reference comp, when one arrived on such a build, is provocation rather than spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is the question of what the image dared that the build did not, and the dares worth adopting enter material_fixes as ordinary ordered fixes. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. 3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition. 4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport. 5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material. diff --git a/.claude/skills/impeccable/reference/degraded/asset-producer.md b/.claude/skills/impeccable/reference/degraded/asset-producer.md index 34829b4ad..cfce6a1f0 100644 --- a/.claude/skills/impeccable/reference/degraded/asset-producer.md +++ b/.claude/skills/impeccable/reference/degraded/asset-producer.md @@ -11,9 +11,9 @@ Your job is production cleanup, not new art direction. Work only from the approv Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster. -## Decision Sketches +## Decision Comps -When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame, including its aspect: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. The frame is shared across siblings so no sketch looks more finished than another; a finish gap breaks the comparison. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a sketch run. +When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `sketch` path (the field keeps its wire name) the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a comp is reported back, not padded from imagination. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (its regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment is what keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run. ## Input Contract diff --git a/.claude/skills/impeccable/reference/degraded/finish-reviewer.md b/.claude/skills/impeccable/reference/degraded/finish-reviewer.md index c49acadb0..c7bb7ca85 100644 --- a/.claude/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.claude/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,12 +11,12 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path; and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order -1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comps with no recorded pick mean the approval point was skipped, and that is a material finding. -2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. +1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comp-round comps with no recorded pick mean the approval point was skipped, and that is a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and they imply no approval whatever the build path; a code-led build has no comp round at all. +2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. A critique-reference comp, when one arrived on such a build, is provocation rather than spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is the question of what the image dared that the build did not, and the dares worth adopting enter material_fixes as ordinary ordered fixes. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. 3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition. 4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport. 5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material. diff --git a/.claude/skills/impeccable/reference/new-work.md b/.claude/skills/impeccable/reference/new-work.md index 718b4bd0b..961d73bb3 100644 --- a/.claude/skills/impeccable/reference/new-work.md +++ b/.claude/skills/impeccable/reference/new-work.md @@ -48,7 +48,7 @@ The script assigns which structure gets built; your top-ranked structure is what The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .claude/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. +When image generation exists, every card also declares a `sketch` path under `.impeccable/mocks/decision/` (the field keeps its wire name for compatibility; what it carries is the card's comp), the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the comps; the page shimmer-waits per slot and the user may answer before they land. Each card's image is that direction's north-star comp at full fidelity, produced under the comp discipline in [visualize.md](visualize.md): the requested surface's first viewport, structure-led prompt, real product name and real content, no invented commercial claims, in that card's own palette, type character, and material world, committed all the way. Generation takes the same time at any fidelity, so an unfinished sketch pays sketch quality for comp cost; fairness between cards comes from equal fidelity in each card's own grammar, one surface, one aspect, never from shared unfinishedness. The frame's aspect is the surface's own: a native app or mobile-first surface comps portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen comped landscape is a broken frame, not a neutral default. Produce in the order the user reads, the assigned card, then the pick, then the full-card hand, then canon, each file written with its prompt sidecar the moment it is done, so a re-roll's spend front-loads onto the cards read first; declined challengers get no comp, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-comp packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. The chosen card's comp is not spent by the choice: on a comp-led build it enters the comp round as compositional option one, and on a code-led build it returns at the finish review as the critique reference, what the image dared that the build did not. The unchosen comps stay in `.impeccable/mocks/decision/` as the round's spent hand; they carry no approval and imply none. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. @@ -82,7 +82,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. @@ -107,6 +107,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .claude/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths, and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .claude/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.claude/skills/impeccable/reference/visualize.md b/.claude/skills/impeccable/reference/visualize.md index 6df08b30b..4d91330e4 100644 --- a/.claude/skills/impeccable/reference/visualize.md +++ b/.claude/skills/impeccable/reference/visualize.md @@ -6,7 +6,7 @@ The purpose of a probe is to test composition, narrative, hierarchy, density, fo ## 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. A decision-page sketch is not a probe: it chose the direction at deliberately unfinished fidelity, so the three comps render regardless, and the chosen card's sketch seeds at most one of them. +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. - 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. - When the user shortlisted multiple concepts, spread the three across them. @@ -22,7 +22,7 @@ Show the three together: in the harness when it can display images, otherwise on Do not begin code until the user approves a direction or explicitly delegates the choice. If they delegate, choose using the task brief, PRODUCT.md, and DESIGN.md, and state the evidence. Approval refines the task concept; it does not modify DESIGN.md. -This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build with generated comps and no recorded approval as carrying a material finding. +This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build whose comp round produced comps with no recorded approval as carrying a material finding; decision comps under `.impeccable/mocks/decision/` are the direction round's hand, not comp-round output, and imply no approval on their own. After approval, record the choice where tools can find it: the approved comp's path goes in the surface brief, and the approved comp's `.json` prompt sidecar gains `"approved": true` (every comp generated through `generate-image.mjs` has one; create it if a native tool didn't). The sidecar travels with the mocks folder, so the approval survives sessions and machines that never see the brief. Then summarize the composition and the parts of the comp that must not be literalized, return to new-work.md, record the direction contract from the approved surface concept, and build. diff --git a/.claude/skills/impeccable/scripts/serve-question.mjs b/.claude/skills/impeccable/scripts/serve-question.mjs index bbbbdf77e..788211920 100644 --- a/.claude/skills/impeccable/scripts/serve-question.mjs +++ b/.claude/skills/impeccable/scripts/serve-question.mjs @@ -42,10 +42,12 @@ * // raise lines under the identity row * "risk": "one line: the honest risk", // optional * "body": "fallback prose when the structured fields are absent", - * "sketch": ".impeccable/sketches/assigned.webp", // optional; may not exist - * // yet: the page shimmer-waits and polls the - * // slot until the file lands, so serve first - * // and generate after + * "sketch": ".impeccable/mocks/decision/assigned.webp", // optional; the card's + * // full-fidelity direction comp (the field + * // keeps the sketch era's wire name). May not + * // exist yet: the page shimmer-waits and + * // polls the slot until the file lands, so + * // serve first and generate after * "hero": "https://... or /abs/path.webp", // optional inspiration image; * // rides picture-in-picture when a sketch exists * "board": "https://... or /abs/path.webp" // optional secondary image @@ -147,7 +149,7 @@ function printAnswer(raw) { console.log("CHOSEN CARD: open the chosen world's board and hero images now, before any code. When your harness only reads files, or runs sandboxed, download them INTO the workspace and open the relative path; a sandboxed viewer rejects absolute paths outside it. They set the craft bar the build must reach."); } if (a.sketch) { - console.log('CHOSEN SKETCH: the decision sketch at that path may seed one comp probe; the comp round still renders its full set, because a sketch chose the direction, not the composition.'); + console.log('CHOSEN COMP: the decision comp at that path is compositional option one. On a comp-led build the comp round adds two variations beside it; on a code-led build it returns at the finish review as the critique reference. Never regenerate it from scratch.'); } if (a.optionId === 'canon') { console.log('CANON CHOSEN: the user picked the category standard on purpose. Ask once for two or three products this should sit alongside; their craft level becomes the quality bar. Execute the canon at full commitment, conventions embraced without irony or smuggled quirk.'); @@ -173,17 +175,17 @@ if (hasFlag('schema')) { title: 'Choose the visual world', question: 'The roll assigned Fillmore Handbill. Keep it, take an alternate, or re-roll.', options: [ - { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, - { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/sketches/model-pick.webp' }, - { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, - { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, + { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/mocks/decision/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, + { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', palette: ['#1f1c18', '#efe5d0', '#a33327'], materials: ['woodcut', 'rag paper'], viewport: 'One tall sheet, the newest release as today’s ballad.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/mocks/decision/model-pick.webp' }, + { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', palette: ['#0000c0', '#ffff00', '#00c000', '#ffffff'], materials: ['block mosaic', 'phosphor glow'], viewport: 'P100 index page, releases as numbered rows.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', risk: 'Reads retro-novelty when the grid is not strict.', sketch: '.impeccable/mocks/decision/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, + { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', palette: ['#101418', '#9fb4c0'], materials: ['film grain', 'backlit glass'], case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, ], reroll: { registers: ['safer', 'bolder'] }, canon: true, - canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', viewport: 'The arrangement a visitor expects, at full craft.', sketch: '.impeccable/sketches/canon.webp' }, + 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.', sketch: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. 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. Sketch 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/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY 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 sketch slot carries the card’s full-fidelity direction comp (the field keeps its wire name for compatibility). 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); } @@ -337,16 +339,19 @@ function loadRound(json) { sketchSrc: sketchSrc(option.sketch), }); options = parsed.options.map(decorate); - // The verdict routes rendering: full cards first, declined cards demoted to - // the end of the deck in their own payload order. The reorder happens here - // so a payload that interleaves them still renders the weighing's shape. + // The verdict routes rendering: full cards first, then the canon, then the + // declined cards dead last in their own payload order. The reorder happens + // here so a payload that interleaves them still renders the weighing's + // shape, and the deck reads as a gradient of standing: contenders, the + // familiar door, then the demoted row. const declined = options.filter((o) => o.verdict === 'declined'); - if (declined.length) options = [...options.filter((o) => o.verdict !== 'declined'), ...declined]; + options = options.filter((o) => o.verdict !== 'declined'); // The standing exit as a full card: same anatomy, reserved id, rendered // subordinate by the page. Without it, canon stays the quiet footer action. if (parsed.canonCard && typeof parsed.canonCard === 'object') { options = [...options, { ...decorate(parsed.canonCard), id: 'canon', isCanon: true }]; } + options = [...options, ...declined]; } try { loadRound(raw); } catch (error) { console.error(`serve-question: ${error.message}`); process.exit(1); } const detachedKey = hasFlag('detached-serve') ? arg('key') : null; @@ -392,10 +397,21 @@ function page() { if (idBits.length) rows.push(`
${idBits.join('')}
`); // Donations from declined challengers render as named raise lines: the // assigned card arrives already raised by the hand it beat, and the raise - // is readable, because a raise nobody can read did not happen. + // is readable, because a raise nobody can read did not happen. One raise + // renders inline; several become a compact cycler (click advances), so a + // generous hand cannot blow the card out of proportion. if (Array.isArray(option.raised) && option.raised.length) { const nameOf = (id) => options.find((o) => o.id === id)?.label || String(id ?? ''); - rows.push(`
${option.raised.slice(0, 4).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`).join('')}
`); + const raiseLines = option.raised.slice(0, 6).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`); + if (raiseLines.length > 1) { + rows.push(`
+
Raised by the hand1/${raiseLines.length}
+ ${raiseLines.join('')} + +
`); + } else { + rows.push(`
${raiseLines[0]}
`); + } } // Demoted art stays reachable as a labeled thumb: the catalog world // explains where the direction comes from without buying it back the @@ -438,7 +454,7 @@ function page() { if (thumbOnly(option)) return ''; if (faceSketch(option)) { return `
-
sketching…
+
rendering…
${inspiration}
${expandChip}${details}
@@ -568,6 +584,10 @@ function page() { .nav.next { right: auto; left: 50%; top: auto; bottom: 6px; transform: translate(-50%, 0); } .fade-prev { top: 0; left: 0; right: 0; bottom: auto; width: auto; height: 72px; background: linear-gradient(180deg, var(--ks-lacquer), transparent); } .fade-next { top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 72px; background: linear-gradient(0deg, var(--ks-lacquer), transparent); } + /* In the vertical deck the cross axis is horizontal: flex-start would + shrink a declined card to content WIDTH, not height, so it stretches + like every other card and its height is already its own. */ + .grid > .card.declined { align-self: stretch; } } .card { position: relative; perspective: 1400px; transform: rotate(var(--fan, 0deg)); transition: transform .25s cubic-bezier(.16, 1, .3, 1); } .card:hover { transform: rotate(0deg) translateY(-4px); } @@ -660,7 +680,10 @@ function page() { and quieter, its catalog art rides as a labeled thumb in the body, and the action reads "Adopt anyway". Adoptable, never deleted: the demoted row is the hand's proof of judgment. */ - .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); } + /* Narrow AND short: without align-self the stretch default drags a thin + declined card to the tallest contender's height, a strange stilt of a + card beside the full hand. */ + .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); align-self: flex-start; } .card.declined .face { background: var(--ks-graphite); } .card.declined:hover .face { border-color: var(--ks-text-faint); } .card.declined h2 { font-size: 1rem; color: var(--ks-text); } @@ -677,6 +700,18 @@ function page() { .raises { display: flex; flex-direction: column; gap: 4px; margin: 2px 0; } .raise { font-size: .78rem; color: var(--ks-text-muted); line-height: 1.45; border-left: 2px solid var(--ks-patina); padding-left: 8px; } .raise .fact-label { color: var(--ks-patina); } + /* Several raises cycle instead of stacking: one visible at a time, a + counter for the rest, the whole block advances on click. */ + .raises-cycle { cursor: pointer; border-radius: 6px; } + .raises-cycle .raise { display: none; border-left: none; padding-left: 0; } + .raises-cycle .raise.active { display: block; } + .raises-cycle { border-left: 2px solid var(--ks-patina); padding-left: 8px; } + .raises-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; } + .raises-head .fact-label { color: var(--ks-patina); } + .raises-count { font-family: var(--ks-mono); font-size: .58rem; letter-spacing: .14em; color: var(--ks-text-faint); } + .raises-count::after { content: " \\203A"; } + .raises-cycle:hover .raises-count { color: var(--ks-patina); } + .sr-live { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } /* The standing exit as a card: present with full anatomy, never dressed as a contender. Graphite instead of kinpaku, and it never takes the lead ring. */ .card.canon .face { border-color: var(--ks-rule); background: var(--ks-graphite); } @@ -776,6 +811,25 @@ function page() { b.closest('.card').classList.toggle('flipped'); })); + // Raise cycler: click (or Enter) advances to the next donation. + document.querySelectorAll('.raises-cycle').forEach(cycle => { + const raises = [...cycle.querySelectorAll('.raise')]; + const count = cycle.querySelector('[data-raises-count]'); + let at = 0; + const live = cycle.querySelector('.sr-live'); + const show = (announce) => { + raises.forEach((raise, i) => raise.classList.toggle('active', i === at)); + if (count) count.textContent = (at + 1) + '/' + raises.length; + // Screen readers hear the raise they just advanced to; the initial + // render stays quiet so page load does not narrate every card. + if (announce && live) live.textContent = 'Raise ' + (at + 1) + ' of ' + raises.length + ': ' + (raises[at]?.textContent || ''); + }; + show(false); + const advance = (e) => { e.stopPropagation(); at = (at + 1) % raises.length; show(true); }; + cycle.addEventListener('click', advance); + cycle.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); advance(e); } }); + }); + // Deal from the stack: cards begin piled at the grid's center, blurred, // then travel to their seats with a stagger. const cards = [...document.querySelectorAll('.card')]; @@ -823,7 +877,7 @@ function page() { const note = m.querySelector('.sketch-note'); const started = Date.now(); // A live elapsed count is the difference between "working" and "frozen". - const tick = setInterval(() => { if (note) note.textContent = 'sketching · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); + const tick = setInterval(() => { if (note) note.textContent = 'rendering · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); const settle = () => { clearInterval(tick); m.classList.remove('sketching', 'stand-in'); m.querySelector('.shimmer')?.remove(); m.querySelector('.stand-in-label')?.remove(); }; const standIn = () => { const pip = m.querySelector('.pip img'); @@ -834,7 +888,7 @@ function page() { clearInterval(tick); const label = document.createElement('p'); label.className = 'stand-in-label'; - label.textContent = 'inspiration · sketch pending'; + label.textContent = 'inspiration · comp pending'; m.appendChild(label); }; const tryLoad = () => { diff --git a/.cursor/agents/impeccable-asset-producer.md b/.cursor/agents/impeccable-asset-producer.md index b56580b4d..cb990e8f5 100644 --- a/.cursor/agents/impeccable-asset-producer.md +++ b/.cursor/agents/impeccable-asset-producer.md @@ -14,9 +14,9 @@ Your job is production cleanup, not new art direction. Work only from the approv Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster. -## Decision Sketches +## Decision Comps -When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame, including its aspect: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. The frame is shared across siblings so no sketch looks more finished than another; a finish gap breaks the comparison. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a sketch run. +When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `sketch` path (the field keeps its wire name) the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a comp is reported back, not padded from imagination. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (its regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment is what keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run. ## Input Contract diff --git a/.cursor/agents/impeccable-finish-reviewer.md b/.cursor/agents/impeccable-finish-reviewer.md index ecbc675d4..b505e5e6b 100644 --- a/.cursor/agents/impeccable-finish-reviewer.md +++ b/.cursor/agents/impeccable-finish-reviewer.md @@ -15,12 +15,12 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path; and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order -1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comps with no recorded pick mean the approval point was skipped, and that is a material finding. -2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. +1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comp-round comps with no recorded pick mean the approval point was skipped, and that is a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and they imply no approval whatever the build path; a code-led build has no comp round at all. +2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. A critique-reference comp, when one arrived on such a build, is provocation rather than spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is the question of what the image dared that the build did not, and the dares worth adopting enter material_fixes as ordinary ordered fixes. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. 3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition. 4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport. 5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material. diff --git a/.cursor/skills/impeccable/reference/degraded/asset-producer.md b/.cursor/skills/impeccable/reference/degraded/asset-producer.md index ae0b40be1..704992d9b 100644 --- a/.cursor/skills/impeccable/reference/degraded/asset-producer.md +++ b/.cursor/skills/impeccable/reference/degraded/asset-producer.md @@ -11,9 +11,9 @@ Your job is production cleanup, not new art direction. Work only from the approv Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster. -## Decision Sketches +## Decision Comps -When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame, including its aspect: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. The frame is shared across siblings so no sketch looks more finished than another; a finish gap breaks the comparison. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a sketch run. +When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `sketch` path (the field keeps its wire name) the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a comp is reported back, not padded from imagination. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (its regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment is what keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run. ## Input Contract diff --git a/.cursor/skills/impeccable/reference/degraded/finish-reviewer.md b/.cursor/skills/impeccable/reference/degraded/finish-reviewer.md index c49acadb0..c7bb7ca85 100644 --- a/.cursor/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.cursor/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,12 +11,12 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path; and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order -1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comps with no recorded pick mean the approval point was skipped, and that is a material finding. -2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. +1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comp-round comps with no recorded pick mean the approval point was skipped, and that is a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and they imply no approval whatever the build path; a code-led build has no comp round at all. +2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. A critique-reference comp, when one arrived on such a build, is provocation rather than spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is the question of what the image dared that the build did not, and the dares worth adopting enter material_fixes as ordinary ordered fixes. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. 3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition. 4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport. 5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material. diff --git a/.cursor/skills/impeccable/reference/new-work.md b/.cursor/skills/impeccable/reference/new-work.md index 0dff0aea7..eccc52c39 100644 --- a/.cursor/skills/impeccable/reference/new-work.md +++ b/.cursor/skills/impeccable/reference/new-work.md @@ -48,7 +48,7 @@ The script assigns which structure gets built; your top-ranked structure is what The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .cursor/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. +When image generation exists, every card also declares a `sketch` path under `.impeccable/mocks/decision/` (the field keeps its wire name for compatibility; what it carries is the card's comp), the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the comps; the page shimmer-waits per slot and the user may answer before they land. Each card's image is that direction's north-star comp at full fidelity, produced under the comp discipline in [visualize.md](visualize.md): the requested surface's first viewport, structure-led prompt, real product name and real content, no invented commercial claims, in that card's own palette, type character, and material world, committed all the way. Generation takes the same time at any fidelity, so an unfinished sketch pays sketch quality for comp cost; fairness between cards comes from equal fidelity in each card's own grammar, one surface, one aspect, never from shared unfinishedness. The frame's aspect is the surface's own: a native app or mobile-first surface comps portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen comped landscape is a broken frame, not a neutral default. Produce in the order the user reads, the assigned card, then the pick, then the full-card hand, then canon, each file written with its prompt sidecar the moment it is done, so a re-roll's spend front-loads onto the cards read first; declined challengers get no comp, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-comp packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. The chosen card's comp is not spent by the choice: on a comp-led build it enters the comp round as compositional option one, and on a code-led build it returns at the finish review as the critique reference, what the image dared that the build did not. The unchosen comps stay in `.impeccable/mocks/decision/` as the round's spent hand; they carry no approval and imply none. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. @@ -80,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. @@ -105,6 +105,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .cursor/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths, and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .cursor/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.cursor/skills/impeccable/reference/visualize.md b/.cursor/skills/impeccable/reference/visualize.md index 3a8cc1e4a..88a21068e 100644 --- a/.cursor/skills/impeccable/reference/visualize.md +++ b/.cursor/skills/impeccable/reference/visualize.md @@ -6,7 +6,7 @@ The purpose of a probe is to test composition, narrative, hierarchy, density, fo ## 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. A decision-page sketch is not a probe: it chose the direction at deliberately unfinished fidelity, so the three comps render regardless, and the chosen card's sketch seeds at most one of them. +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. - 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. - When the user shortlisted multiple concepts, spread the three across them. @@ -22,7 +22,7 @@ Show the three together: in the harness when it can display images, otherwise on Do not begin code until the user approves a direction or explicitly delegates the choice. If they delegate, choose using the task brief, PRODUCT.md, and DESIGN.md, and state the evidence. Approval refines the task concept; it does not modify DESIGN.md. -This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build with generated comps and no recorded approval as carrying a material finding. +This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build whose comp round produced comps with no recorded approval as carrying a material finding; decision comps under `.impeccable/mocks/decision/` are the direction round's hand, not comp-round output, and imply no approval on their own. After approval, record the choice where tools can find it: the approved comp's path goes in the surface brief, and the approved comp's `.json` prompt sidecar gains `"approved": true` (every comp generated through `generate-image.mjs` has one; create it if a native tool didn't). The sidecar travels with the mocks folder, so the approval survives sessions and machines that never see the brief. Then summarize the composition and the parts of the comp that must not be literalized, return to new-work.md, record the direction contract from the approved surface concept, and build. diff --git a/.cursor/skills/impeccable/scripts/serve-question.mjs b/.cursor/skills/impeccable/scripts/serve-question.mjs index bbbbdf77e..788211920 100644 --- a/.cursor/skills/impeccable/scripts/serve-question.mjs +++ b/.cursor/skills/impeccable/scripts/serve-question.mjs @@ -42,10 +42,12 @@ * // raise lines under the identity row * "risk": "one line: the honest risk", // optional * "body": "fallback prose when the structured fields are absent", - * "sketch": ".impeccable/sketches/assigned.webp", // optional; may not exist - * // yet: the page shimmer-waits and polls the - * // slot until the file lands, so serve first - * // and generate after + * "sketch": ".impeccable/mocks/decision/assigned.webp", // optional; the card's + * // full-fidelity direction comp (the field + * // keeps the sketch era's wire name). May not + * // exist yet: the page shimmer-waits and + * // polls the slot until the file lands, so + * // serve first and generate after * "hero": "https://... or /abs/path.webp", // optional inspiration image; * // rides picture-in-picture when a sketch exists * "board": "https://... or /abs/path.webp" // optional secondary image @@ -147,7 +149,7 @@ function printAnswer(raw) { console.log("CHOSEN CARD: open the chosen world's board and hero images now, before any code. When your harness only reads files, or runs sandboxed, download them INTO the workspace and open the relative path; a sandboxed viewer rejects absolute paths outside it. They set the craft bar the build must reach."); } if (a.sketch) { - console.log('CHOSEN SKETCH: the decision sketch at that path may seed one comp probe; the comp round still renders its full set, because a sketch chose the direction, not the composition.'); + console.log('CHOSEN COMP: the decision comp at that path is compositional option one. On a comp-led build the comp round adds two variations beside it; on a code-led build it returns at the finish review as the critique reference. Never regenerate it from scratch.'); } if (a.optionId === 'canon') { console.log('CANON CHOSEN: the user picked the category standard on purpose. Ask once for two or three products this should sit alongside; their craft level becomes the quality bar. Execute the canon at full commitment, conventions embraced without irony or smuggled quirk.'); @@ -173,17 +175,17 @@ if (hasFlag('schema')) { title: 'Choose the visual world', question: 'The roll assigned Fillmore Handbill. Keep it, take an alternate, or re-roll.', options: [ - { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, - { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/sketches/model-pick.webp' }, - { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, - { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, + { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/mocks/decision/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, + { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', palette: ['#1f1c18', '#efe5d0', '#a33327'], materials: ['woodcut', 'rag paper'], viewport: 'One tall sheet, the newest release as today’s ballad.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/mocks/decision/model-pick.webp' }, + { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', palette: ['#0000c0', '#ffff00', '#00c000', '#ffffff'], materials: ['block mosaic', 'phosphor glow'], viewport: 'P100 index page, releases as numbered rows.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', risk: 'Reads retro-novelty when the grid is not strict.', sketch: '.impeccable/mocks/decision/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, + { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', palette: ['#101418', '#9fb4c0'], materials: ['film grain', 'backlit glass'], case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, ], reroll: { registers: ['safer', 'bolder'] }, canon: true, - canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', viewport: 'The arrangement a visitor expects, at full craft.', sketch: '.impeccable/sketches/canon.webp' }, + 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.', sketch: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. 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. Sketch 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/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY 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 sketch slot carries the card’s full-fidelity direction comp (the field keeps its wire name for compatibility). 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); } @@ -337,16 +339,19 @@ function loadRound(json) { sketchSrc: sketchSrc(option.sketch), }); options = parsed.options.map(decorate); - // The verdict routes rendering: full cards first, declined cards demoted to - // the end of the deck in their own payload order. The reorder happens here - // so a payload that interleaves them still renders the weighing's shape. + // The verdict routes rendering: full cards first, then the canon, then the + // declined cards dead last in their own payload order. The reorder happens + // here so a payload that interleaves them still renders the weighing's + // shape, and the deck reads as a gradient of standing: contenders, the + // familiar door, then the demoted row. const declined = options.filter((o) => o.verdict === 'declined'); - if (declined.length) options = [...options.filter((o) => o.verdict !== 'declined'), ...declined]; + options = options.filter((o) => o.verdict !== 'declined'); // The standing exit as a full card: same anatomy, reserved id, rendered // subordinate by the page. Without it, canon stays the quiet footer action. if (parsed.canonCard && typeof parsed.canonCard === 'object') { options = [...options, { ...decorate(parsed.canonCard), id: 'canon', isCanon: true }]; } + options = [...options, ...declined]; } try { loadRound(raw); } catch (error) { console.error(`serve-question: ${error.message}`); process.exit(1); } const detachedKey = hasFlag('detached-serve') ? arg('key') : null; @@ -392,10 +397,21 @@ function page() { if (idBits.length) rows.push(`
${idBits.join('')}
`); // Donations from declined challengers render as named raise lines: the // assigned card arrives already raised by the hand it beat, and the raise - // is readable, because a raise nobody can read did not happen. + // is readable, because a raise nobody can read did not happen. One raise + // renders inline; several become a compact cycler (click advances), so a + // generous hand cannot blow the card out of proportion. if (Array.isArray(option.raised) && option.raised.length) { const nameOf = (id) => options.find((o) => o.id === id)?.label || String(id ?? ''); - rows.push(`
${option.raised.slice(0, 4).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`).join('')}
`); + const raiseLines = option.raised.slice(0, 6).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`); + if (raiseLines.length > 1) { + rows.push(`
+
Raised by the hand1/${raiseLines.length}
+ ${raiseLines.join('')} + +
`); + } else { + rows.push(`
${raiseLines[0]}
`); + } } // Demoted art stays reachable as a labeled thumb: the catalog world // explains where the direction comes from without buying it back the @@ -438,7 +454,7 @@ function page() { if (thumbOnly(option)) return ''; if (faceSketch(option)) { return `
-
sketching…
+
rendering…
${inspiration}
${expandChip}${details}
@@ -568,6 +584,10 @@ function page() { .nav.next { right: auto; left: 50%; top: auto; bottom: 6px; transform: translate(-50%, 0); } .fade-prev { top: 0; left: 0; right: 0; bottom: auto; width: auto; height: 72px; background: linear-gradient(180deg, var(--ks-lacquer), transparent); } .fade-next { top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 72px; background: linear-gradient(0deg, var(--ks-lacquer), transparent); } + /* In the vertical deck the cross axis is horizontal: flex-start would + shrink a declined card to content WIDTH, not height, so it stretches + like every other card and its height is already its own. */ + .grid > .card.declined { align-self: stretch; } } .card { position: relative; perspective: 1400px; transform: rotate(var(--fan, 0deg)); transition: transform .25s cubic-bezier(.16, 1, .3, 1); } .card:hover { transform: rotate(0deg) translateY(-4px); } @@ -660,7 +680,10 @@ function page() { and quieter, its catalog art rides as a labeled thumb in the body, and the action reads "Adopt anyway". Adoptable, never deleted: the demoted row is the hand's proof of judgment. */ - .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); } + /* Narrow AND short: without align-self the stretch default drags a thin + declined card to the tallest contender's height, a strange stilt of a + card beside the full hand. */ + .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); align-self: flex-start; } .card.declined .face { background: var(--ks-graphite); } .card.declined:hover .face { border-color: var(--ks-text-faint); } .card.declined h2 { font-size: 1rem; color: var(--ks-text); } @@ -677,6 +700,18 @@ function page() { .raises { display: flex; flex-direction: column; gap: 4px; margin: 2px 0; } .raise { font-size: .78rem; color: var(--ks-text-muted); line-height: 1.45; border-left: 2px solid var(--ks-patina); padding-left: 8px; } .raise .fact-label { color: var(--ks-patina); } + /* Several raises cycle instead of stacking: one visible at a time, a + counter for the rest, the whole block advances on click. */ + .raises-cycle { cursor: pointer; border-radius: 6px; } + .raises-cycle .raise { display: none; border-left: none; padding-left: 0; } + .raises-cycle .raise.active { display: block; } + .raises-cycle { border-left: 2px solid var(--ks-patina); padding-left: 8px; } + .raises-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; } + .raises-head .fact-label { color: var(--ks-patina); } + .raises-count { font-family: var(--ks-mono); font-size: .58rem; letter-spacing: .14em; color: var(--ks-text-faint); } + .raises-count::after { content: " \\203A"; } + .raises-cycle:hover .raises-count { color: var(--ks-patina); } + .sr-live { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } /* The standing exit as a card: present with full anatomy, never dressed as a contender. Graphite instead of kinpaku, and it never takes the lead ring. */ .card.canon .face { border-color: var(--ks-rule); background: var(--ks-graphite); } @@ -776,6 +811,25 @@ function page() { b.closest('.card').classList.toggle('flipped'); })); + // Raise cycler: click (or Enter) advances to the next donation. + document.querySelectorAll('.raises-cycle').forEach(cycle => { + const raises = [...cycle.querySelectorAll('.raise')]; + const count = cycle.querySelector('[data-raises-count]'); + let at = 0; + const live = cycle.querySelector('.sr-live'); + const show = (announce) => { + raises.forEach((raise, i) => raise.classList.toggle('active', i === at)); + if (count) count.textContent = (at + 1) + '/' + raises.length; + // Screen readers hear the raise they just advanced to; the initial + // render stays quiet so page load does not narrate every card. + if (announce && live) live.textContent = 'Raise ' + (at + 1) + ' of ' + raises.length + ': ' + (raises[at]?.textContent || ''); + }; + show(false); + const advance = (e) => { e.stopPropagation(); at = (at + 1) % raises.length; show(true); }; + cycle.addEventListener('click', advance); + cycle.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); advance(e); } }); + }); + // Deal from the stack: cards begin piled at the grid's center, blurred, // then travel to their seats with a stagger. const cards = [...document.querySelectorAll('.card')]; @@ -823,7 +877,7 @@ function page() { const note = m.querySelector('.sketch-note'); const started = Date.now(); // A live elapsed count is the difference between "working" and "frozen". - const tick = setInterval(() => { if (note) note.textContent = 'sketching · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); + const tick = setInterval(() => { if (note) note.textContent = 'rendering · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); const settle = () => { clearInterval(tick); m.classList.remove('sketching', 'stand-in'); m.querySelector('.shimmer')?.remove(); m.querySelector('.stand-in-label')?.remove(); }; const standIn = () => { const pip = m.querySelector('.pip img'); @@ -834,7 +888,7 @@ function page() { clearInterval(tick); const label = document.createElement('p'); label.className = 'stand-in-label'; - label.textContent = 'inspiration · sketch pending'; + label.textContent = 'inspiration · comp pending'; m.appendChild(label); }; const tryLoad = () => { diff --git a/.gemini/skills/impeccable/reference/degraded/asset-producer.md b/.gemini/skills/impeccable/reference/degraded/asset-producer.md index ca44a05f7..ae4f613a7 100644 --- a/.gemini/skills/impeccable/reference/degraded/asset-producer.md +++ b/.gemini/skills/impeccable/reference/degraded/asset-producer.md @@ -11,9 +11,9 @@ Your job is production cleanup, not new art direction. Work only from the approv Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster. -## Decision Sketches +## Decision Comps -When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame, including its aspect: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. The frame is shared across siblings so no sketch looks more finished than another; a finish gap breaks the comparison. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a sketch run. +When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `sketch` path (the field keeps its wire name) the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a comp is reported back, not padded from imagination. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (its regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment is what keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run. ## Input Contract diff --git a/.gemini/skills/impeccable/reference/degraded/finish-reviewer.md b/.gemini/skills/impeccable/reference/degraded/finish-reviewer.md index c49acadb0..c7bb7ca85 100644 --- a/.gemini/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.gemini/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,12 +11,12 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path; and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order -1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comps with no recorded pick mean the approval point was skipped, and that is a material finding. -2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. +1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comp-round comps with no recorded pick mean the approval point was skipped, and that is a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and they imply no approval whatever the build path; a code-led build has no comp round at all. +2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. A critique-reference comp, when one arrived on such a build, is provocation rather than spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is the question of what the image dared that the build did not, and the dares worth adopting enter material_fixes as ordinary ordered fixes. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. 3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition. 4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport. 5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material. diff --git a/.gemini/skills/impeccable/reference/new-work.md b/.gemini/skills/impeccable/reference/new-work.md index 08a8ed6f0..0034d3689 100644 --- a/.gemini/skills/impeccable/reference/new-work.md +++ b/.gemini/skills/impeccable/reference/new-work.md @@ -48,7 +48,7 @@ The script assigns which structure gets built; your top-ranked structure is what The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .gemini/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. +When image generation exists, every card also declares a `sketch` path under `.impeccable/mocks/decision/` (the field keeps its wire name for compatibility; what it carries is the card's comp), the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the comps; the page shimmer-waits per slot and the user may answer before they land. Each card's image is that direction's north-star comp at full fidelity, produced under the comp discipline in [visualize.md](visualize.md): the requested surface's first viewport, structure-led prompt, real product name and real content, no invented commercial claims, in that card's own palette, type character, and material world, committed all the way. Generation takes the same time at any fidelity, so an unfinished sketch pays sketch quality for comp cost; fairness between cards comes from equal fidelity in each card's own grammar, one surface, one aspect, never from shared unfinishedness. The frame's aspect is the surface's own: a native app or mobile-first surface comps portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen comped landscape is a broken frame, not a neutral default. Produce in the order the user reads, the assigned card, then the pick, then the full-card hand, then canon, each file written with its prompt sidecar the moment it is done, so a re-roll's spend front-loads onto the cards read first; declined challengers get no comp, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-comp packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. The chosen card's comp is not spent by the choice: on a comp-led build it enters the comp round as compositional option one, and on a code-led build it returns at the finish review as the critique reference, what the image dared that the build did not. The unchosen comps stay in `.impeccable/mocks/decision/` as the round's spent hand; they carry no approval and imply none. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. @@ -80,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. @@ -105,6 +105,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .gemini/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths, and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .gemini/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.gemini/skills/impeccable/reference/visualize.md b/.gemini/skills/impeccable/reference/visualize.md index 82f2cc4de..c5e5fa5cc 100644 --- a/.gemini/skills/impeccable/reference/visualize.md +++ b/.gemini/skills/impeccable/reference/visualize.md @@ -6,7 +6,7 @@ The purpose of a probe is to test composition, narrative, hierarchy, density, fo ## 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. A decision-page sketch is not a probe: it chose the direction at deliberately unfinished fidelity, so the three comps render regardless, and the chosen card's sketch seeds at most one of them. +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. - 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. - When the user shortlisted multiple concepts, spread the three across them. @@ -22,7 +22,7 @@ Show the three together: in the harness when it can display images, otherwise on Do not begin code until the user approves a direction or explicitly delegates the choice. If they delegate, choose using the task brief, PRODUCT.md, and DESIGN.md, and state the evidence. Approval refines the task concept; it does not modify DESIGN.md. -This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build with generated comps and no recorded approval as carrying a material finding. +This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build whose comp round produced comps with no recorded approval as carrying a material finding; decision comps under `.impeccable/mocks/decision/` are the direction round's hand, not comp-round output, and imply no approval on their own. After approval, record the choice where tools can find it: the approved comp's path goes in the surface brief, and the approved comp's `.json` prompt sidecar gains `"approved": true` (every comp generated through `generate-image.mjs` has one; create it if a native tool didn't). The sidecar travels with the mocks folder, so the approval survives sessions and machines that never see the brief. Then summarize the composition and the parts of the comp that must not be literalized, return to new-work.md, record the direction contract from the approved surface concept, and build. diff --git a/.gemini/skills/impeccable/scripts/serve-question.mjs b/.gemini/skills/impeccable/scripts/serve-question.mjs index bbbbdf77e..788211920 100644 --- a/.gemini/skills/impeccable/scripts/serve-question.mjs +++ b/.gemini/skills/impeccable/scripts/serve-question.mjs @@ -42,10 +42,12 @@ * // raise lines under the identity row * "risk": "one line: the honest risk", // optional * "body": "fallback prose when the structured fields are absent", - * "sketch": ".impeccable/sketches/assigned.webp", // optional; may not exist - * // yet: the page shimmer-waits and polls the - * // slot until the file lands, so serve first - * // and generate after + * "sketch": ".impeccable/mocks/decision/assigned.webp", // optional; the card's + * // full-fidelity direction comp (the field + * // keeps the sketch era's wire name). May not + * // exist yet: the page shimmer-waits and + * // polls the slot until the file lands, so + * // serve first and generate after * "hero": "https://... or /abs/path.webp", // optional inspiration image; * // rides picture-in-picture when a sketch exists * "board": "https://... or /abs/path.webp" // optional secondary image @@ -147,7 +149,7 @@ function printAnswer(raw) { console.log("CHOSEN CARD: open the chosen world's board and hero images now, before any code. When your harness only reads files, or runs sandboxed, download them INTO the workspace and open the relative path; a sandboxed viewer rejects absolute paths outside it. They set the craft bar the build must reach."); } if (a.sketch) { - console.log('CHOSEN SKETCH: the decision sketch at that path may seed one comp probe; the comp round still renders its full set, because a sketch chose the direction, not the composition.'); + console.log('CHOSEN COMP: the decision comp at that path is compositional option one. On a comp-led build the comp round adds two variations beside it; on a code-led build it returns at the finish review as the critique reference. Never regenerate it from scratch.'); } if (a.optionId === 'canon') { console.log('CANON CHOSEN: the user picked the category standard on purpose. Ask once for two or three products this should sit alongside; their craft level becomes the quality bar. Execute the canon at full commitment, conventions embraced without irony or smuggled quirk.'); @@ -173,17 +175,17 @@ if (hasFlag('schema')) { title: 'Choose the visual world', question: 'The roll assigned Fillmore Handbill. Keep it, take an alternate, or re-roll.', options: [ - { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, - { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/sketches/model-pick.webp' }, - { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, - { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, + { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/mocks/decision/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, + { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', palette: ['#1f1c18', '#efe5d0', '#a33327'], materials: ['woodcut', 'rag paper'], viewport: 'One tall sheet, the newest release as today’s ballad.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/mocks/decision/model-pick.webp' }, + { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', palette: ['#0000c0', '#ffff00', '#00c000', '#ffffff'], materials: ['block mosaic', 'phosphor glow'], viewport: 'P100 index page, releases as numbered rows.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', risk: 'Reads retro-novelty when the grid is not strict.', sketch: '.impeccable/mocks/decision/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, + { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', palette: ['#101418', '#9fb4c0'], materials: ['film grain', 'backlit glass'], case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, ], reroll: { registers: ['safer', 'bolder'] }, canon: true, - canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', viewport: 'The arrangement a visitor expects, at full craft.', sketch: '.impeccable/sketches/canon.webp' }, + 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.', sketch: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. 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. Sketch 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/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY 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 sketch slot carries the card’s full-fidelity direction comp (the field keeps its wire name for compatibility). 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); } @@ -337,16 +339,19 @@ function loadRound(json) { sketchSrc: sketchSrc(option.sketch), }); options = parsed.options.map(decorate); - // The verdict routes rendering: full cards first, declined cards demoted to - // the end of the deck in their own payload order. The reorder happens here - // so a payload that interleaves them still renders the weighing's shape. + // The verdict routes rendering: full cards first, then the canon, then the + // declined cards dead last in their own payload order. The reorder happens + // here so a payload that interleaves them still renders the weighing's + // shape, and the deck reads as a gradient of standing: contenders, the + // familiar door, then the demoted row. const declined = options.filter((o) => o.verdict === 'declined'); - if (declined.length) options = [...options.filter((o) => o.verdict !== 'declined'), ...declined]; + options = options.filter((o) => o.verdict !== 'declined'); // The standing exit as a full card: same anatomy, reserved id, rendered // subordinate by the page. Without it, canon stays the quiet footer action. if (parsed.canonCard && typeof parsed.canonCard === 'object') { options = [...options, { ...decorate(parsed.canonCard), id: 'canon', isCanon: true }]; } + options = [...options, ...declined]; } try { loadRound(raw); } catch (error) { console.error(`serve-question: ${error.message}`); process.exit(1); } const detachedKey = hasFlag('detached-serve') ? arg('key') : null; @@ -392,10 +397,21 @@ function page() { if (idBits.length) rows.push(`
${idBits.join('')}
`); // Donations from declined challengers render as named raise lines: the // assigned card arrives already raised by the hand it beat, and the raise - // is readable, because a raise nobody can read did not happen. + // is readable, because a raise nobody can read did not happen. One raise + // renders inline; several become a compact cycler (click advances), so a + // generous hand cannot blow the card out of proportion. if (Array.isArray(option.raised) && option.raised.length) { const nameOf = (id) => options.find((o) => o.id === id)?.label || String(id ?? ''); - rows.push(`
${option.raised.slice(0, 4).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`).join('')}
`); + const raiseLines = option.raised.slice(0, 6).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`); + if (raiseLines.length > 1) { + rows.push(`
+
Raised by the hand1/${raiseLines.length}
+ ${raiseLines.join('')} + +
`); + } else { + rows.push(`
${raiseLines[0]}
`); + } } // Demoted art stays reachable as a labeled thumb: the catalog world // explains where the direction comes from without buying it back the @@ -438,7 +454,7 @@ function page() { if (thumbOnly(option)) return ''; if (faceSketch(option)) { return `
-
sketching…
+
rendering…
${inspiration}
${expandChip}${details}
@@ -568,6 +584,10 @@ function page() { .nav.next { right: auto; left: 50%; top: auto; bottom: 6px; transform: translate(-50%, 0); } .fade-prev { top: 0; left: 0; right: 0; bottom: auto; width: auto; height: 72px; background: linear-gradient(180deg, var(--ks-lacquer), transparent); } .fade-next { top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 72px; background: linear-gradient(0deg, var(--ks-lacquer), transparent); } + /* In the vertical deck the cross axis is horizontal: flex-start would + shrink a declined card to content WIDTH, not height, so it stretches + like every other card and its height is already its own. */ + .grid > .card.declined { align-self: stretch; } } .card { position: relative; perspective: 1400px; transform: rotate(var(--fan, 0deg)); transition: transform .25s cubic-bezier(.16, 1, .3, 1); } .card:hover { transform: rotate(0deg) translateY(-4px); } @@ -660,7 +680,10 @@ function page() { and quieter, its catalog art rides as a labeled thumb in the body, and the action reads "Adopt anyway". Adoptable, never deleted: the demoted row is the hand's proof of judgment. */ - .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); } + /* Narrow AND short: without align-self the stretch default drags a thin + declined card to the tallest contender's height, a strange stilt of a + card beside the full hand. */ + .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); align-self: flex-start; } .card.declined .face { background: var(--ks-graphite); } .card.declined:hover .face { border-color: var(--ks-text-faint); } .card.declined h2 { font-size: 1rem; color: var(--ks-text); } @@ -677,6 +700,18 @@ function page() { .raises { display: flex; flex-direction: column; gap: 4px; margin: 2px 0; } .raise { font-size: .78rem; color: var(--ks-text-muted); line-height: 1.45; border-left: 2px solid var(--ks-patina); padding-left: 8px; } .raise .fact-label { color: var(--ks-patina); } + /* Several raises cycle instead of stacking: one visible at a time, a + counter for the rest, the whole block advances on click. */ + .raises-cycle { cursor: pointer; border-radius: 6px; } + .raises-cycle .raise { display: none; border-left: none; padding-left: 0; } + .raises-cycle .raise.active { display: block; } + .raises-cycle { border-left: 2px solid var(--ks-patina); padding-left: 8px; } + .raises-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; } + .raises-head .fact-label { color: var(--ks-patina); } + .raises-count { font-family: var(--ks-mono); font-size: .58rem; letter-spacing: .14em; color: var(--ks-text-faint); } + .raises-count::after { content: " \\203A"; } + .raises-cycle:hover .raises-count { color: var(--ks-patina); } + .sr-live { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } /* The standing exit as a card: present with full anatomy, never dressed as a contender. Graphite instead of kinpaku, and it never takes the lead ring. */ .card.canon .face { border-color: var(--ks-rule); background: var(--ks-graphite); } @@ -776,6 +811,25 @@ function page() { b.closest('.card').classList.toggle('flipped'); })); + // Raise cycler: click (or Enter) advances to the next donation. + document.querySelectorAll('.raises-cycle').forEach(cycle => { + const raises = [...cycle.querySelectorAll('.raise')]; + const count = cycle.querySelector('[data-raises-count]'); + let at = 0; + const live = cycle.querySelector('.sr-live'); + const show = (announce) => { + raises.forEach((raise, i) => raise.classList.toggle('active', i === at)); + if (count) count.textContent = (at + 1) + '/' + raises.length; + // Screen readers hear the raise they just advanced to; the initial + // render stays quiet so page load does not narrate every card. + if (announce && live) live.textContent = 'Raise ' + (at + 1) + ' of ' + raises.length + ': ' + (raises[at]?.textContent || ''); + }; + show(false); + const advance = (e) => { e.stopPropagation(); at = (at + 1) % raises.length; show(true); }; + cycle.addEventListener('click', advance); + cycle.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); advance(e); } }); + }); + // Deal from the stack: cards begin piled at the grid's center, blurred, // then travel to their seats with a stagger. const cards = [...document.querySelectorAll('.card')]; @@ -823,7 +877,7 @@ function page() { const note = m.querySelector('.sketch-note'); const started = Date.now(); // A live elapsed count is the difference between "working" and "frozen". - const tick = setInterval(() => { if (note) note.textContent = 'sketching · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); + const tick = setInterval(() => { if (note) note.textContent = 'rendering · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); const settle = () => { clearInterval(tick); m.classList.remove('sketching', 'stand-in'); m.querySelector('.shimmer')?.remove(); m.querySelector('.stand-in-label')?.remove(); }; const standIn = () => { const pip = m.querySelector('.pip img'); @@ -834,7 +888,7 @@ function page() { clearInterval(tick); const label = document.createElement('p'); label.className = 'stand-in-label'; - label.textContent = 'inspiration · sketch pending'; + label.textContent = 'inspiration · comp pending'; m.appendChild(label); }; const tryLoad = () => { diff --git a/.github/skills/impeccable/reference/degraded/asset-producer.md b/.github/skills/impeccable/reference/degraded/asset-producer.md index e3f6c5ea7..ecfc2ca8b 100644 --- a/.github/skills/impeccable/reference/degraded/asset-producer.md +++ b/.github/skills/impeccable/reference/degraded/asset-producer.md @@ -11,9 +11,9 @@ Your job is production cleanup, not new art direction. Work only from the approv Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster. -## Decision Sketches +## Decision Comps -When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame, including its aspect: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. The frame is shared across siblings so no sketch looks more finished than another; a finish gap breaks the comparison. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a sketch run. +When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `sketch` path (the field keeps its wire name) the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a comp is reported back, not padded from imagination. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (its regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment is what keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run. ## Input Contract diff --git a/.github/skills/impeccable/reference/degraded/finish-reviewer.md b/.github/skills/impeccable/reference/degraded/finish-reviewer.md index c49acadb0..c7bb7ca85 100644 --- a/.github/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.github/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,12 +11,12 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path; and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order -1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comps with no recorded pick mean the approval point was skipped, and that is a material finding. -2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. +1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comp-round comps with no recorded pick mean the approval point was skipped, and that is a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and they imply no approval whatever the build path; a code-led build has no comp round at all. +2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. A critique-reference comp, when one arrived on such a build, is provocation rather than spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is the question of what the image dared that the build did not, and the dares worth adopting enter material_fixes as ordinary ordered fixes. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. 3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition. 4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport. 5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material. diff --git a/.github/skills/impeccable/reference/new-work.md b/.github/skills/impeccable/reference/new-work.md index 959eb081e..9b27efb47 100644 --- a/.github/skills/impeccable/reference/new-work.md +++ b/.github/skills/impeccable/reference/new-work.md @@ -48,7 +48,7 @@ The script assigns which structure gets built; your top-ranked structure is what The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .github/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. +When image generation exists, every card also declares a `sketch` path under `.impeccable/mocks/decision/` (the field keeps its wire name for compatibility; what it carries is the card's comp), the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the comps; the page shimmer-waits per slot and the user may answer before they land. Each card's image is that direction's north-star comp at full fidelity, produced under the comp discipline in [visualize.md](visualize.md): the requested surface's first viewport, structure-led prompt, real product name and real content, no invented commercial claims, in that card's own palette, type character, and material world, committed all the way. Generation takes the same time at any fidelity, so an unfinished sketch pays sketch quality for comp cost; fairness between cards comes from equal fidelity in each card's own grammar, one surface, one aspect, never from shared unfinishedness. The frame's aspect is the surface's own: a native app or mobile-first surface comps portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen comped landscape is a broken frame, not a neutral default. Produce in the order the user reads, the assigned card, then the pick, then the full-card hand, then canon, each file written with its prompt sidecar the moment it is done, so a re-roll's spend front-loads onto the cards read first; declined challengers get no comp, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-comp packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. The chosen card's comp is not spent by the choice: on a comp-led build it enters the comp round as compositional option one, and on a code-led build it returns at the finish review as the critique reference, what the image dared that the build did not. The unchosen comps stay in `.impeccable/mocks/decision/` as the round's spent hand; they carry no approval and imply none. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. @@ -80,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. @@ -105,6 +105,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .github/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths, and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .github/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.github/skills/impeccable/reference/visualize.md b/.github/skills/impeccable/reference/visualize.md index 60ee2020a..ad538ba5d 100644 --- a/.github/skills/impeccable/reference/visualize.md +++ b/.github/skills/impeccable/reference/visualize.md @@ -6,7 +6,7 @@ The purpose of a probe is to test composition, narrative, hierarchy, density, fo ## 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. A decision-page sketch is not a probe: it chose the direction at deliberately unfinished fidelity, so the three comps render regardless, and the chosen card's sketch seeds at most one of them. +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. - 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. - When the user shortlisted multiple concepts, spread the three across them. @@ -22,7 +22,7 @@ Show the three together: in the harness when it can display images, otherwise on Do not begin code until the user approves a direction or explicitly delegates the choice. If they delegate, choose using the task brief, PRODUCT.md, and DESIGN.md, and state the evidence. Approval refines the task concept; it does not modify DESIGN.md. -This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build with generated comps and no recorded approval as carrying a material finding. +This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build whose comp round produced comps with no recorded approval as carrying a material finding; decision comps under `.impeccable/mocks/decision/` are the direction round's hand, not comp-round output, and imply no approval on their own. After approval, record the choice where tools can find it: the approved comp's path goes in the surface brief, and the approved comp's `.json` prompt sidecar gains `"approved": true` (every comp generated through `generate-image.mjs` has one; create it if a native tool didn't). The sidecar travels with the mocks folder, so the approval survives sessions and machines that never see the brief. Then summarize the composition and the parts of the comp that must not be literalized, return to new-work.md, record the direction contract from the approved surface concept, and build. diff --git a/.github/skills/impeccable/scripts/serve-question.mjs b/.github/skills/impeccable/scripts/serve-question.mjs index bbbbdf77e..788211920 100644 --- a/.github/skills/impeccable/scripts/serve-question.mjs +++ b/.github/skills/impeccable/scripts/serve-question.mjs @@ -42,10 +42,12 @@ * // raise lines under the identity row * "risk": "one line: the honest risk", // optional * "body": "fallback prose when the structured fields are absent", - * "sketch": ".impeccable/sketches/assigned.webp", // optional; may not exist - * // yet: the page shimmer-waits and polls the - * // slot until the file lands, so serve first - * // and generate after + * "sketch": ".impeccable/mocks/decision/assigned.webp", // optional; the card's + * // full-fidelity direction comp (the field + * // keeps the sketch era's wire name). May not + * // exist yet: the page shimmer-waits and + * // polls the slot until the file lands, so + * // serve first and generate after * "hero": "https://... or /abs/path.webp", // optional inspiration image; * // rides picture-in-picture when a sketch exists * "board": "https://... or /abs/path.webp" // optional secondary image @@ -147,7 +149,7 @@ function printAnswer(raw) { console.log("CHOSEN CARD: open the chosen world's board and hero images now, before any code. When your harness only reads files, or runs sandboxed, download them INTO the workspace and open the relative path; a sandboxed viewer rejects absolute paths outside it. They set the craft bar the build must reach."); } if (a.sketch) { - console.log('CHOSEN SKETCH: the decision sketch at that path may seed one comp probe; the comp round still renders its full set, because a sketch chose the direction, not the composition.'); + console.log('CHOSEN COMP: the decision comp at that path is compositional option one. On a comp-led build the comp round adds two variations beside it; on a code-led build it returns at the finish review as the critique reference. Never regenerate it from scratch.'); } if (a.optionId === 'canon') { console.log('CANON CHOSEN: the user picked the category standard on purpose. Ask once for two or three products this should sit alongside; their craft level becomes the quality bar. Execute the canon at full commitment, conventions embraced without irony or smuggled quirk.'); @@ -173,17 +175,17 @@ if (hasFlag('schema')) { title: 'Choose the visual world', question: 'The roll assigned Fillmore Handbill. Keep it, take an alternate, or re-roll.', options: [ - { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, - { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/sketches/model-pick.webp' }, - { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, - { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, + { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/mocks/decision/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, + { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', palette: ['#1f1c18', '#efe5d0', '#a33327'], materials: ['woodcut', 'rag paper'], viewport: 'One tall sheet, the newest release as today’s ballad.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/mocks/decision/model-pick.webp' }, + { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', palette: ['#0000c0', '#ffff00', '#00c000', '#ffffff'], materials: ['block mosaic', 'phosphor glow'], viewport: 'P100 index page, releases as numbered rows.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', risk: 'Reads retro-novelty when the grid is not strict.', sketch: '.impeccable/mocks/decision/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, + { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', palette: ['#101418', '#9fb4c0'], materials: ['film grain', 'backlit glass'], case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, ], reroll: { registers: ['safer', 'bolder'] }, canon: true, - canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', viewport: 'The arrangement a visitor expects, at full craft.', sketch: '.impeccable/sketches/canon.webp' }, + 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.', sketch: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. 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. Sketch 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/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY 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 sketch slot carries the card’s full-fidelity direction comp (the field keeps its wire name for compatibility). 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); } @@ -337,16 +339,19 @@ function loadRound(json) { sketchSrc: sketchSrc(option.sketch), }); options = parsed.options.map(decorate); - // The verdict routes rendering: full cards first, declined cards demoted to - // the end of the deck in their own payload order. The reorder happens here - // so a payload that interleaves them still renders the weighing's shape. + // The verdict routes rendering: full cards first, then the canon, then the + // declined cards dead last in their own payload order. The reorder happens + // here so a payload that interleaves them still renders the weighing's + // shape, and the deck reads as a gradient of standing: contenders, the + // familiar door, then the demoted row. const declined = options.filter((o) => o.verdict === 'declined'); - if (declined.length) options = [...options.filter((o) => o.verdict !== 'declined'), ...declined]; + options = options.filter((o) => o.verdict !== 'declined'); // The standing exit as a full card: same anatomy, reserved id, rendered // subordinate by the page. Without it, canon stays the quiet footer action. if (parsed.canonCard && typeof parsed.canonCard === 'object') { options = [...options, { ...decorate(parsed.canonCard), id: 'canon', isCanon: true }]; } + options = [...options, ...declined]; } try { loadRound(raw); } catch (error) { console.error(`serve-question: ${error.message}`); process.exit(1); } const detachedKey = hasFlag('detached-serve') ? arg('key') : null; @@ -392,10 +397,21 @@ function page() { if (idBits.length) rows.push(`
${idBits.join('')}
`); // Donations from declined challengers render as named raise lines: the // assigned card arrives already raised by the hand it beat, and the raise - // is readable, because a raise nobody can read did not happen. + // is readable, because a raise nobody can read did not happen. One raise + // renders inline; several become a compact cycler (click advances), so a + // generous hand cannot blow the card out of proportion. if (Array.isArray(option.raised) && option.raised.length) { const nameOf = (id) => options.find((o) => o.id === id)?.label || String(id ?? ''); - rows.push(`
${option.raised.slice(0, 4).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`).join('')}
`); + const raiseLines = option.raised.slice(0, 6).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`); + if (raiseLines.length > 1) { + rows.push(`
+
Raised by the hand1/${raiseLines.length}
+ ${raiseLines.join('')} + +
`); + } else { + rows.push(`
${raiseLines[0]}
`); + } } // Demoted art stays reachable as a labeled thumb: the catalog world // explains where the direction comes from without buying it back the @@ -438,7 +454,7 @@ function page() { if (thumbOnly(option)) return ''; if (faceSketch(option)) { return `
-
sketching…
+
rendering…
${inspiration}
${expandChip}${details}
@@ -568,6 +584,10 @@ function page() { .nav.next { right: auto; left: 50%; top: auto; bottom: 6px; transform: translate(-50%, 0); } .fade-prev { top: 0; left: 0; right: 0; bottom: auto; width: auto; height: 72px; background: linear-gradient(180deg, var(--ks-lacquer), transparent); } .fade-next { top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 72px; background: linear-gradient(0deg, var(--ks-lacquer), transparent); } + /* In the vertical deck the cross axis is horizontal: flex-start would + shrink a declined card to content WIDTH, not height, so it stretches + like every other card and its height is already its own. */ + .grid > .card.declined { align-self: stretch; } } .card { position: relative; perspective: 1400px; transform: rotate(var(--fan, 0deg)); transition: transform .25s cubic-bezier(.16, 1, .3, 1); } .card:hover { transform: rotate(0deg) translateY(-4px); } @@ -660,7 +680,10 @@ function page() { and quieter, its catalog art rides as a labeled thumb in the body, and the action reads "Adopt anyway". Adoptable, never deleted: the demoted row is the hand's proof of judgment. */ - .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); } + /* Narrow AND short: without align-self the stretch default drags a thin + declined card to the tallest contender's height, a strange stilt of a + card beside the full hand. */ + .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); align-self: flex-start; } .card.declined .face { background: var(--ks-graphite); } .card.declined:hover .face { border-color: var(--ks-text-faint); } .card.declined h2 { font-size: 1rem; color: var(--ks-text); } @@ -677,6 +700,18 @@ function page() { .raises { display: flex; flex-direction: column; gap: 4px; margin: 2px 0; } .raise { font-size: .78rem; color: var(--ks-text-muted); line-height: 1.45; border-left: 2px solid var(--ks-patina); padding-left: 8px; } .raise .fact-label { color: var(--ks-patina); } + /* Several raises cycle instead of stacking: one visible at a time, a + counter for the rest, the whole block advances on click. */ + .raises-cycle { cursor: pointer; border-radius: 6px; } + .raises-cycle .raise { display: none; border-left: none; padding-left: 0; } + .raises-cycle .raise.active { display: block; } + .raises-cycle { border-left: 2px solid var(--ks-patina); padding-left: 8px; } + .raises-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; } + .raises-head .fact-label { color: var(--ks-patina); } + .raises-count { font-family: var(--ks-mono); font-size: .58rem; letter-spacing: .14em; color: var(--ks-text-faint); } + .raises-count::after { content: " \\203A"; } + .raises-cycle:hover .raises-count { color: var(--ks-patina); } + .sr-live { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } /* The standing exit as a card: present with full anatomy, never dressed as a contender. Graphite instead of kinpaku, and it never takes the lead ring. */ .card.canon .face { border-color: var(--ks-rule); background: var(--ks-graphite); } @@ -776,6 +811,25 @@ function page() { b.closest('.card').classList.toggle('flipped'); })); + // Raise cycler: click (or Enter) advances to the next donation. + document.querySelectorAll('.raises-cycle').forEach(cycle => { + const raises = [...cycle.querySelectorAll('.raise')]; + const count = cycle.querySelector('[data-raises-count]'); + let at = 0; + const live = cycle.querySelector('.sr-live'); + const show = (announce) => { + raises.forEach((raise, i) => raise.classList.toggle('active', i === at)); + if (count) count.textContent = (at + 1) + '/' + raises.length; + // Screen readers hear the raise they just advanced to; the initial + // render stays quiet so page load does not narrate every card. + if (announce && live) live.textContent = 'Raise ' + (at + 1) + ' of ' + raises.length + ': ' + (raises[at]?.textContent || ''); + }; + show(false); + const advance = (e) => { e.stopPropagation(); at = (at + 1) % raises.length; show(true); }; + cycle.addEventListener('click', advance); + cycle.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); advance(e); } }); + }); + // Deal from the stack: cards begin piled at the grid's center, blurred, // then travel to their seats with a stagger. const cards = [...document.querySelectorAll('.card')]; @@ -823,7 +877,7 @@ function page() { const note = m.querySelector('.sketch-note'); const started = Date.now(); // A live elapsed count is the difference between "working" and "frozen". - const tick = setInterval(() => { if (note) note.textContent = 'sketching · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); + const tick = setInterval(() => { if (note) note.textContent = 'rendering · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); const settle = () => { clearInterval(tick); m.classList.remove('sketching', 'stand-in'); m.querySelector('.shimmer')?.remove(); m.querySelector('.stand-in-label')?.remove(); }; const standIn = () => { const pip = m.querySelector('.pip img'); @@ -834,7 +888,7 @@ function page() { clearInterval(tick); const label = document.createElement('p'); label.className = 'stand-in-label'; - label.textContent = 'inspiration · sketch pending'; + label.textContent = 'inspiration · comp pending'; m.appendChild(label); }; const tryLoad = () => { diff --git a/.grok/agents/impeccable-asset-producer.md b/.grok/agents/impeccable-asset-producer.md index e151d3596..ef77e7cf2 100644 --- a/.grok/agents/impeccable-asset-producer.md +++ b/.grok/agents/impeccable-asset-producer.md @@ -16,9 +16,9 @@ Your job is production cleanup, not new art direction. Work only from the approv Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster. -## Decision Sketches +## Decision Comps -When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame, including its aspect: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. The frame is shared across siblings so no sketch looks more finished than another; a finish gap breaks the comparison. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a sketch run. +When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `sketch` path (the field keeps its wire name) the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a comp is reported back, not padded from imagination. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (its regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment is what keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run. ## Input Contract diff --git a/.grok/agents/impeccable-finish-reviewer.md b/.grok/agents/impeccable-finish-reviewer.md index 7c71679c1..02658bf8e 100644 --- a/.grok/agents/impeccable-finish-reviewer.md +++ b/.grok/agents/impeccable-finish-reviewer.md @@ -16,12 +16,12 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path; and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order -1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comps with no recorded pick mean the approval point was skipped, and that is a material finding. -2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. +1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comp-round comps with no recorded pick mean the approval point was skipped, and that is a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and they imply no approval whatever the build path; a code-led build has no comp round at all. +2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. A critique-reference comp, when one arrived on such a build, is provocation rather than spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is the question of what the image dared that the build did not, and the dares worth adopting enter material_fixes as ordinary ordered fixes. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. 3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition. 4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport. 5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material. diff --git a/.grok/skills/impeccable/reference/degraded/asset-producer.md b/.grok/skills/impeccable/reference/degraded/asset-producer.md index 966d11b7a..f73ac2c35 100644 --- a/.grok/skills/impeccable/reference/degraded/asset-producer.md +++ b/.grok/skills/impeccable/reference/degraded/asset-producer.md @@ -11,9 +11,9 @@ Your job is production cleanup, not new art direction. Work only from the approv Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster. -## Decision Sketches +## Decision Comps -When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame, including its aspect: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. The frame is shared across siblings so no sketch looks more finished than another; a finish gap breaks the comparison. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a sketch run. +When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `sketch` path (the field keeps its wire name) the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a comp is reported back, not padded from imagination. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (its regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment is what keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run. ## Input Contract diff --git a/.grok/skills/impeccable/reference/degraded/finish-reviewer.md b/.grok/skills/impeccable/reference/degraded/finish-reviewer.md index c49acadb0..c7bb7ca85 100644 --- a/.grok/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.grok/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,12 +11,12 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path; and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order -1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comps with no recorded pick mean the approval point was skipped, and that is a material finding. -2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. +1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comp-round comps with no recorded pick mean the approval point was skipped, and that is a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and they imply no approval whatever the build path; a code-led build has no comp round at all. +2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. A critique-reference comp, when one arrived on such a build, is provocation rather than spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is the question of what the image dared that the build did not, and the dares worth adopting enter material_fixes as ordinary ordered fixes. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. 3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition. 4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport. 5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material. diff --git a/.grok/skills/impeccable/reference/new-work.md b/.grok/skills/impeccable/reference/new-work.md index 0e92394b7..6cbf96844 100644 --- a/.grok/skills/impeccable/reference/new-work.md +++ b/.grok/skills/impeccable/reference/new-work.md @@ -48,7 +48,7 @@ The script assigns which structure gets built; your top-ranked structure is what The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .grok/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. +When image generation exists, every card also declares a `sketch` path under `.impeccable/mocks/decision/` (the field keeps its wire name for compatibility; what it carries is the card's comp), the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the comps; the page shimmer-waits per slot and the user may answer before they land. Each card's image is that direction's north-star comp at full fidelity, produced under the comp discipline in [visualize.md](visualize.md): the requested surface's first viewport, structure-led prompt, real product name and real content, no invented commercial claims, in that card's own palette, type character, and material world, committed all the way. Generation takes the same time at any fidelity, so an unfinished sketch pays sketch quality for comp cost; fairness between cards comes from equal fidelity in each card's own grammar, one surface, one aspect, never from shared unfinishedness. The frame's aspect is the surface's own: a native app or mobile-first surface comps portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen comped landscape is a broken frame, not a neutral default. Produce in the order the user reads, the assigned card, then the pick, then the full-card hand, then canon, each file written with its prompt sidecar the moment it is done, so a re-roll's spend front-loads onto the cards read first; declined challengers get no comp, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-comp packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. The chosen card's comp is not spent by the choice: on a comp-led build it enters the comp round as compositional option one, and on a code-led build it returns at the finish review as the critique reference, what the image dared that the build did not. The unchosen comps stay in `.impeccable/mocks/decision/` as the round's spent hand; they carry no approval and imply none. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. @@ -80,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. @@ -105,6 +105,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .grok/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths, and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .grok/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.grok/skills/impeccable/reference/visualize.md b/.grok/skills/impeccable/reference/visualize.md index 063520a6e..33790fe43 100644 --- a/.grok/skills/impeccable/reference/visualize.md +++ b/.grok/skills/impeccable/reference/visualize.md @@ -6,7 +6,7 @@ The purpose of a probe is to test composition, narrative, hierarchy, density, fo ## 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. A decision-page sketch is not a probe: it chose the direction at deliberately unfinished fidelity, so the three comps render regardless, and the chosen card's sketch seeds at most one of them. +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. - 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. - When the user shortlisted multiple concepts, spread the three across them. @@ -22,7 +22,7 @@ Show the three together: in the harness when it can display images, otherwise on Do not begin code until the user approves a direction or explicitly delegates the choice. If they delegate, choose using the task brief, PRODUCT.md, and DESIGN.md, and state the evidence. Approval refines the task concept; it does not modify DESIGN.md. -This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build with generated comps and no recorded approval as carrying a material finding. +This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build whose comp round produced comps with no recorded approval as carrying a material finding; decision comps under `.impeccable/mocks/decision/` are the direction round's hand, not comp-round output, and imply no approval on their own. After approval, record the choice where tools can find it: the approved comp's path goes in the surface brief, and the approved comp's `.json` prompt sidecar gains `"approved": true` (every comp generated through `generate-image.mjs` has one; create it if a native tool didn't). The sidecar travels with the mocks folder, so the approval survives sessions and machines that never see the brief. Then summarize the composition and the parts of the comp that must not be literalized, return to new-work.md, record the direction contract from the approved surface concept, and build. diff --git a/.grok/skills/impeccable/scripts/serve-question.mjs b/.grok/skills/impeccable/scripts/serve-question.mjs index bbbbdf77e..788211920 100644 --- a/.grok/skills/impeccable/scripts/serve-question.mjs +++ b/.grok/skills/impeccable/scripts/serve-question.mjs @@ -42,10 +42,12 @@ * // raise lines under the identity row * "risk": "one line: the honest risk", // optional * "body": "fallback prose when the structured fields are absent", - * "sketch": ".impeccable/sketches/assigned.webp", // optional; may not exist - * // yet: the page shimmer-waits and polls the - * // slot until the file lands, so serve first - * // and generate after + * "sketch": ".impeccable/mocks/decision/assigned.webp", // optional; the card's + * // full-fidelity direction comp (the field + * // keeps the sketch era's wire name). May not + * // exist yet: the page shimmer-waits and + * // polls the slot until the file lands, so + * // serve first and generate after * "hero": "https://... or /abs/path.webp", // optional inspiration image; * // rides picture-in-picture when a sketch exists * "board": "https://... or /abs/path.webp" // optional secondary image @@ -147,7 +149,7 @@ function printAnswer(raw) { console.log("CHOSEN CARD: open the chosen world's board and hero images now, before any code. When your harness only reads files, or runs sandboxed, download them INTO the workspace and open the relative path; a sandboxed viewer rejects absolute paths outside it. They set the craft bar the build must reach."); } if (a.sketch) { - console.log('CHOSEN SKETCH: the decision sketch at that path may seed one comp probe; the comp round still renders its full set, because a sketch chose the direction, not the composition.'); + console.log('CHOSEN COMP: the decision comp at that path is compositional option one. On a comp-led build the comp round adds two variations beside it; on a code-led build it returns at the finish review as the critique reference. Never regenerate it from scratch.'); } if (a.optionId === 'canon') { console.log('CANON CHOSEN: the user picked the category standard on purpose. Ask once for two or three products this should sit alongside; their craft level becomes the quality bar. Execute the canon at full commitment, conventions embraced without irony or smuggled quirk.'); @@ -173,17 +175,17 @@ if (hasFlag('schema')) { title: 'Choose the visual world', question: 'The roll assigned Fillmore Handbill. Keep it, take an alternate, or re-roll.', options: [ - { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, - { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/sketches/model-pick.webp' }, - { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, - { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, + { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/mocks/decision/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, + { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', palette: ['#1f1c18', '#efe5d0', '#a33327'], materials: ['woodcut', 'rag paper'], viewport: 'One tall sheet, the newest release as today’s ballad.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/mocks/decision/model-pick.webp' }, + { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', palette: ['#0000c0', '#ffff00', '#00c000', '#ffffff'], materials: ['block mosaic', 'phosphor glow'], viewport: 'P100 index page, releases as numbered rows.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', risk: 'Reads retro-novelty when the grid is not strict.', sketch: '.impeccable/mocks/decision/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, + { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', palette: ['#101418', '#9fb4c0'], materials: ['film grain', 'backlit glass'], case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, ], reroll: { registers: ['safer', 'bolder'] }, canon: true, - canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', viewport: 'The arrangement a visitor expects, at full craft.', sketch: '.impeccable/sketches/canon.webp' }, + 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.', sketch: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. 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. Sketch 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/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY 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 sketch slot carries the card’s full-fidelity direction comp (the field keeps its wire name for compatibility). 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); } @@ -337,16 +339,19 @@ function loadRound(json) { sketchSrc: sketchSrc(option.sketch), }); options = parsed.options.map(decorate); - // The verdict routes rendering: full cards first, declined cards demoted to - // the end of the deck in their own payload order. The reorder happens here - // so a payload that interleaves them still renders the weighing's shape. + // The verdict routes rendering: full cards first, then the canon, then the + // declined cards dead last in their own payload order. The reorder happens + // here so a payload that interleaves them still renders the weighing's + // shape, and the deck reads as a gradient of standing: contenders, the + // familiar door, then the demoted row. const declined = options.filter((o) => o.verdict === 'declined'); - if (declined.length) options = [...options.filter((o) => o.verdict !== 'declined'), ...declined]; + options = options.filter((o) => o.verdict !== 'declined'); // The standing exit as a full card: same anatomy, reserved id, rendered // subordinate by the page. Without it, canon stays the quiet footer action. if (parsed.canonCard && typeof parsed.canonCard === 'object') { options = [...options, { ...decorate(parsed.canonCard), id: 'canon', isCanon: true }]; } + options = [...options, ...declined]; } try { loadRound(raw); } catch (error) { console.error(`serve-question: ${error.message}`); process.exit(1); } const detachedKey = hasFlag('detached-serve') ? arg('key') : null; @@ -392,10 +397,21 @@ function page() { if (idBits.length) rows.push(`
${idBits.join('')}
`); // Donations from declined challengers render as named raise lines: the // assigned card arrives already raised by the hand it beat, and the raise - // is readable, because a raise nobody can read did not happen. + // is readable, because a raise nobody can read did not happen. One raise + // renders inline; several become a compact cycler (click advances), so a + // generous hand cannot blow the card out of proportion. if (Array.isArray(option.raised) && option.raised.length) { const nameOf = (id) => options.find((o) => o.id === id)?.label || String(id ?? ''); - rows.push(`
${option.raised.slice(0, 4).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`).join('')}
`); + const raiseLines = option.raised.slice(0, 6).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`); + if (raiseLines.length > 1) { + rows.push(`
+
Raised by the hand1/${raiseLines.length}
+ ${raiseLines.join('')} + +
`); + } else { + rows.push(`
${raiseLines[0]}
`); + } } // Demoted art stays reachable as a labeled thumb: the catalog world // explains where the direction comes from without buying it back the @@ -438,7 +454,7 @@ function page() { if (thumbOnly(option)) return ''; if (faceSketch(option)) { return `
-
sketching…
+
rendering…
${inspiration}
${expandChip}${details}
@@ -568,6 +584,10 @@ function page() { .nav.next { right: auto; left: 50%; top: auto; bottom: 6px; transform: translate(-50%, 0); } .fade-prev { top: 0; left: 0; right: 0; bottom: auto; width: auto; height: 72px; background: linear-gradient(180deg, var(--ks-lacquer), transparent); } .fade-next { top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 72px; background: linear-gradient(0deg, var(--ks-lacquer), transparent); } + /* In the vertical deck the cross axis is horizontal: flex-start would + shrink a declined card to content WIDTH, not height, so it stretches + like every other card and its height is already its own. */ + .grid > .card.declined { align-self: stretch; } } .card { position: relative; perspective: 1400px; transform: rotate(var(--fan, 0deg)); transition: transform .25s cubic-bezier(.16, 1, .3, 1); } .card:hover { transform: rotate(0deg) translateY(-4px); } @@ -660,7 +680,10 @@ function page() { and quieter, its catalog art rides as a labeled thumb in the body, and the action reads "Adopt anyway". Adoptable, never deleted: the demoted row is the hand's proof of judgment. */ - .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); } + /* Narrow AND short: without align-self the stretch default drags a thin + declined card to the tallest contender's height, a strange stilt of a + card beside the full hand. */ + .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); align-self: flex-start; } .card.declined .face { background: var(--ks-graphite); } .card.declined:hover .face { border-color: var(--ks-text-faint); } .card.declined h2 { font-size: 1rem; color: var(--ks-text); } @@ -677,6 +700,18 @@ function page() { .raises { display: flex; flex-direction: column; gap: 4px; margin: 2px 0; } .raise { font-size: .78rem; color: var(--ks-text-muted); line-height: 1.45; border-left: 2px solid var(--ks-patina); padding-left: 8px; } .raise .fact-label { color: var(--ks-patina); } + /* Several raises cycle instead of stacking: one visible at a time, a + counter for the rest, the whole block advances on click. */ + .raises-cycle { cursor: pointer; border-radius: 6px; } + .raises-cycle .raise { display: none; border-left: none; padding-left: 0; } + .raises-cycle .raise.active { display: block; } + .raises-cycle { border-left: 2px solid var(--ks-patina); padding-left: 8px; } + .raises-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; } + .raises-head .fact-label { color: var(--ks-patina); } + .raises-count { font-family: var(--ks-mono); font-size: .58rem; letter-spacing: .14em; color: var(--ks-text-faint); } + .raises-count::after { content: " \\203A"; } + .raises-cycle:hover .raises-count { color: var(--ks-patina); } + .sr-live { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } /* The standing exit as a card: present with full anatomy, never dressed as a contender. Graphite instead of kinpaku, and it never takes the lead ring. */ .card.canon .face { border-color: var(--ks-rule); background: var(--ks-graphite); } @@ -776,6 +811,25 @@ function page() { b.closest('.card').classList.toggle('flipped'); })); + // Raise cycler: click (or Enter) advances to the next donation. + document.querySelectorAll('.raises-cycle').forEach(cycle => { + const raises = [...cycle.querySelectorAll('.raise')]; + const count = cycle.querySelector('[data-raises-count]'); + let at = 0; + const live = cycle.querySelector('.sr-live'); + const show = (announce) => { + raises.forEach((raise, i) => raise.classList.toggle('active', i === at)); + if (count) count.textContent = (at + 1) + '/' + raises.length; + // Screen readers hear the raise they just advanced to; the initial + // render stays quiet so page load does not narrate every card. + if (announce && live) live.textContent = 'Raise ' + (at + 1) + ' of ' + raises.length + ': ' + (raises[at]?.textContent || ''); + }; + show(false); + const advance = (e) => { e.stopPropagation(); at = (at + 1) % raises.length; show(true); }; + cycle.addEventListener('click', advance); + cycle.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); advance(e); } }); + }); + // Deal from the stack: cards begin piled at the grid's center, blurred, // then travel to their seats with a stagger. const cards = [...document.querySelectorAll('.card')]; @@ -823,7 +877,7 @@ function page() { const note = m.querySelector('.sketch-note'); const started = Date.now(); // A live elapsed count is the difference between "working" and "frozen". - const tick = setInterval(() => { if (note) note.textContent = 'sketching · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); + const tick = setInterval(() => { if (note) note.textContent = 'rendering · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); const settle = () => { clearInterval(tick); m.classList.remove('sketching', 'stand-in'); m.querySelector('.shimmer')?.remove(); m.querySelector('.stand-in-label')?.remove(); }; const standIn = () => { const pip = m.querySelector('.pip img'); @@ -834,7 +888,7 @@ function page() { clearInterval(tick); const label = document.createElement('p'); label.className = 'stand-in-label'; - label.textContent = 'inspiration · sketch pending'; + label.textContent = 'inspiration · comp pending'; m.appendChild(label); }; const tryLoad = () => { diff --git a/.kiro/skills/impeccable/reference/degraded/asset-producer.md b/.kiro/skills/impeccable/reference/degraded/asset-producer.md index d15839aae..286ef7130 100644 --- a/.kiro/skills/impeccable/reference/degraded/asset-producer.md +++ b/.kiro/skills/impeccable/reference/degraded/asset-producer.md @@ -11,9 +11,9 @@ Your job is production cleanup, not new art direction. Work only from the approv Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster. -## Decision Sketches +## Decision Comps -When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame, including its aspect: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. The frame is shared across siblings so no sketch looks more finished than another; a finish gap breaks the comparison. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a sketch run. +When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `sketch` path (the field keeps its wire name) the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a comp is reported back, not padded from imagination. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (its regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment is what keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run. ## Input Contract diff --git a/.kiro/skills/impeccable/reference/degraded/finish-reviewer.md b/.kiro/skills/impeccable/reference/degraded/finish-reviewer.md index c49acadb0..c7bb7ca85 100644 --- a/.kiro/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.kiro/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,12 +11,12 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path; and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order -1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comps with no recorded pick mean the approval point was skipped, and that is a material finding. -2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. +1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comp-round comps with no recorded pick mean the approval point was skipped, and that is a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and they imply no approval whatever the build path; a code-led build has no comp round at all. +2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. A critique-reference comp, when one arrived on such a build, is provocation rather than spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is the question of what the image dared that the build did not, and the dares worth adopting enter material_fixes as ordinary ordered fixes. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. 3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition. 4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport. 5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material. diff --git a/.kiro/skills/impeccable/reference/new-work.md b/.kiro/skills/impeccable/reference/new-work.md index 2402bc90a..618bc6f66 100644 --- a/.kiro/skills/impeccable/reference/new-work.md +++ b/.kiro/skills/impeccable/reference/new-work.md @@ -48,7 +48,7 @@ The script assigns which structure gets built; your top-ranked structure is what The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .kiro/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. +When image generation exists, every card also declares a `sketch` path under `.impeccable/mocks/decision/` (the field keeps its wire name for compatibility; what it carries is the card's comp), the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the comps; the page shimmer-waits per slot and the user may answer before they land. Each card's image is that direction's north-star comp at full fidelity, produced under the comp discipline in [visualize.md](visualize.md): the requested surface's first viewport, structure-led prompt, real product name and real content, no invented commercial claims, in that card's own palette, type character, and material world, committed all the way. Generation takes the same time at any fidelity, so an unfinished sketch pays sketch quality for comp cost; fairness between cards comes from equal fidelity in each card's own grammar, one surface, one aspect, never from shared unfinishedness. The frame's aspect is the surface's own: a native app or mobile-first surface comps portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen comped landscape is a broken frame, not a neutral default. Produce in the order the user reads, the assigned card, then the pick, then the full-card hand, then canon, each file written with its prompt sidecar the moment it is done, so a re-roll's spend front-loads onto the cards read first; declined challengers get no comp, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-comp packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. The chosen card's comp is not spent by the choice: on a comp-led build it enters the comp round as compositional option one, and on a code-led build it returns at the finish review as the critique reference, what the image dared that the build did not. The unchosen comps stay in `.impeccable/mocks/decision/` as the round's spent hand; they carry no approval and imply none. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. @@ -80,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. @@ -105,6 +105,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .kiro/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths, and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .kiro/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.kiro/skills/impeccable/reference/visualize.md b/.kiro/skills/impeccable/reference/visualize.md index cc0057342..d0388bd0a 100644 --- a/.kiro/skills/impeccable/reference/visualize.md +++ b/.kiro/skills/impeccable/reference/visualize.md @@ -6,7 +6,7 @@ The purpose of a probe is to test composition, narrative, hierarchy, density, fo ## 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. A decision-page sketch is not a probe: it chose the direction at deliberately unfinished fidelity, so the three comps render regardless, and the chosen card's sketch seeds at most one of them. +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. - 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. - When the user shortlisted multiple concepts, spread the three across them. @@ -22,7 +22,7 @@ Show the three together: in the harness when it can display images, otherwise on Do not begin code until the user approves a direction or explicitly delegates the choice. If they delegate, choose using the task brief, PRODUCT.md, and DESIGN.md, and state the evidence. Approval refines the task concept; it does not modify DESIGN.md. -This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build with generated comps and no recorded approval as carrying a material finding. +This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build whose comp round produced comps with no recorded approval as carrying a material finding; decision comps under `.impeccable/mocks/decision/` are the direction round's hand, not comp-round output, and imply no approval on their own. After approval, record the choice where tools can find it: the approved comp's path goes in the surface brief, and the approved comp's `.json` prompt sidecar gains `"approved": true` (every comp generated through `generate-image.mjs` has one; create it if a native tool didn't). The sidecar travels with the mocks folder, so the approval survives sessions and machines that never see the brief. Then summarize the composition and the parts of the comp that must not be literalized, return to new-work.md, record the direction contract from the approved surface concept, and build. diff --git a/.kiro/skills/impeccable/scripts/serve-question.mjs b/.kiro/skills/impeccable/scripts/serve-question.mjs index bbbbdf77e..788211920 100644 --- a/.kiro/skills/impeccable/scripts/serve-question.mjs +++ b/.kiro/skills/impeccable/scripts/serve-question.mjs @@ -42,10 +42,12 @@ * // raise lines under the identity row * "risk": "one line: the honest risk", // optional * "body": "fallback prose when the structured fields are absent", - * "sketch": ".impeccable/sketches/assigned.webp", // optional; may not exist - * // yet: the page shimmer-waits and polls the - * // slot until the file lands, so serve first - * // and generate after + * "sketch": ".impeccable/mocks/decision/assigned.webp", // optional; the card's + * // full-fidelity direction comp (the field + * // keeps the sketch era's wire name). May not + * // exist yet: the page shimmer-waits and + * // polls the slot until the file lands, so + * // serve first and generate after * "hero": "https://... or /abs/path.webp", // optional inspiration image; * // rides picture-in-picture when a sketch exists * "board": "https://... or /abs/path.webp" // optional secondary image @@ -147,7 +149,7 @@ function printAnswer(raw) { console.log("CHOSEN CARD: open the chosen world's board and hero images now, before any code. When your harness only reads files, or runs sandboxed, download them INTO the workspace and open the relative path; a sandboxed viewer rejects absolute paths outside it. They set the craft bar the build must reach."); } if (a.sketch) { - console.log('CHOSEN SKETCH: the decision sketch at that path may seed one comp probe; the comp round still renders its full set, because a sketch chose the direction, not the composition.'); + console.log('CHOSEN COMP: the decision comp at that path is compositional option one. On a comp-led build the comp round adds two variations beside it; on a code-led build it returns at the finish review as the critique reference. Never regenerate it from scratch.'); } if (a.optionId === 'canon') { console.log('CANON CHOSEN: the user picked the category standard on purpose. Ask once for two or three products this should sit alongside; their craft level becomes the quality bar. Execute the canon at full commitment, conventions embraced without irony or smuggled quirk.'); @@ -173,17 +175,17 @@ if (hasFlag('schema')) { title: 'Choose the visual world', question: 'The roll assigned Fillmore Handbill. Keep it, take an alternate, or re-roll.', options: [ - { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, - { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/sketches/model-pick.webp' }, - { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, - { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, + { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/mocks/decision/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, + { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', palette: ['#1f1c18', '#efe5d0', '#a33327'], materials: ['woodcut', 'rag paper'], viewport: 'One tall sheet, the newest release as today’s ballad.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/mocks/decision/model-pick.webp' }, + { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', palette: ['#0000c0', '#ffff00', '#00c000', '#ffffff'], materials: ['block mosaic', 'phosphor glow'], viewport: 'P100 index page, releases as numbered rows.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', risk: 'Reads retro-novelty when the grid is not strict.', sketch: '.impeccable/mocks/decision/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, + { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', palette: ['#101418', '#9fb4c0'], materials: ['film grain', 'backlit glass'], case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, ], reroll: { registers: ['safer', 'bolder'] }, canon: true, - canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', viewport: 'The arrangement a visitor expects, at full craft.', sketch: '.impeccable/sketches/canon.webp' }, + 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.', sketch: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. 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. Sketch 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/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY 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 sketch slot carries the card’s full-fidelity direction comp (the field keeps its wire name for compatibility). 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); } @@ -337,16 +339,19 @@ function loadRound(json) { sketchSrc: sketchSrc(option.sketch), }); options = parsed.options.map(decorate); - // The verdict routes rendering: full cards first, declined cards demoted to - // the end of the deck in their own payload order. The reorder happens here - // so a payload that interleaves them still renders the weighing's shape. + // The verdict routes rendering: full cards first, then the canon, then the + // declined cards dead last in their own payload order. The reorder happens + // here so a payload that interleaves them still renders the weighing's + // shape, and the deck reads as a gradient of standing: contenders, the + // familiar door, then the demoted row. const declined = options.filter((o) => o.verdict === 'declined'); - if (declined.length) options = [...options.filter((o) => o.verdict !== 'declined'), ...declined]; + options = options.filter((o) => o.verdict !== 'declined'); // The standing exit as a full card: same anatomy, reserved id, rendered // subordinate by the page. Without it, canon stays the quiet footer action. if (parsed.canonCard && typeof parsed.canonCard === 'object') { options = [...options, { ...decorate(parsed.canonCard), id: 'canon', isCanon: true }]; } + options = [...options, ...declined]; } try { loadRound(raw); } catch (error) { console.error(`serve-question: ${error.message}`); process.exit(1); } const detachedKey = hasFlag('detached-serve') ? arg('key') : null; @@ -392,10 +397,21 @@ function page() { if (idBits.length) rows.push(`
${idBits.join('')}
`); // Donations from declined challengers render as named raise lines: the // assigned card arrives already raised by the hand it beat, and the raise - // is readable, because a raise nobody can read did not happen. + // is readable, because a raise nobody can read did not happen. One raise + // renders inline; several become a compact cycler (click advances), so a + // generous hand cannot blow the card out of proportion. if (Array.isArray(option.raised) && option.raised.length) { const nameOf = (id) => options.find((o) => o.id === id)?.label || String(id ?? ''); - rows.push(`
${option.raised.slice(0, 4).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`).join('')}
`); + const raiseLines = option.raised.slice(0, 6).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`); + if (raiseLines.length > 1) { + rows.push(`
+
Raised by the hand1/${raiseLines.length}
+ ${raiseLines.join('')} + +
`); + } else { + rows.push(`
${raiseLines[0]}
`); + } } // Demoted art stays reachable as a labeled thumb: the catalog world // explains where the direction comes from without buying it back the @@ -438,7 +454,7 @@ function page() { if (thumbOnly(option)) return ''; if (faceSketch(option)) { return `
-
sketching…
+
rendering…
${inspiration}
${expandChip}${details}
@@ -568,6 +584,10 @@ function page() { .nav.next { right: auto; left: 50%; top: auto; bottom: 6px; transform: translate(-50%, 0); } .fade-prev { top: 0; left: 0; right: 0; bottom: auto; width: auto; height: 72px; background: linear-gradient(180deg, var(--ks-lacquer), transparent); } .fade-next { top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 72px; background: linear-gradient(0deg, var(--ks-lacquer), transparent); } + /* In the vertical deck the cross axis is horizontal: flex-start would + shrink a declined card to content WIDTH, not height, so it stretches + like every other card and its height is already its own. */ + .grid > .card.declined { align-self: stretch; } } .card { position: relative; perspective: 1400px; transform: rotate(var(--fan, 0deg)); transition: transform .25s cubic-bezier(.16, 1, .3, 1); } .card:hover { transform: rotate(0deg) translateY(-4px); } @@ -660,7 +680,10 @@ function page() { and quieter, its catalog art rides as a labeled thumb in the body, and the action reads "Adopt anyway". Adoptable, never deleted: the demoted row is the hand's proof of judgment. */ - .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); } + /* Narrow AND short: without align-self the stretch default drags a thin + declined card to the tallest contender's height, a strange stilt of a + card beside the full hand. */ + .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); align-self: flex-start; } .card.declined .face { background: var(--ks-graphite); } .card.declined:hover .face { border-color: var(--ks-text-faint); } .card.declined h2 { font-size: 1rem; color: var(--ks-text); } @@ -677,6 +700,18 @@ function page() { .raises { display: flex; flex-direction: column; gap: 4px; margin: 2px 0; } .raise { font-size: .78rem; color: var(--ks-text-muted); line-height: 1.45; border-left: 2px solid var(--ks-patina); padding-left: 8px; } .raise .fact-label { color: var(--ks-patina); } + /* Several raises cycle instead of stacking: one visible at a time, a + counter for the rest, the whole block advances on click. */ + .raises-cycle { cursor: pointer; border-radius: 6px; } + .raises-cycle .raise { display: none; border-left: none; padding-left: 0; } + .raises-cycle .raise.active { display: block; } + .raises-cycle { border-left: 2px solid var(--ks-patina); padding-left: 8px; } + .raises-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; } + .raises-head .fact-label { color: var(--ks-patina); } + .raises-count { font-family: var(--ks-mono); font-size: .58rem; letter-spacing: .14em; color: var(--ks-text-faint); } + .raises-count::after { content: " \\203A"; } + .raises-cycle:hover .raises-count { color: var(--ks-patina); } + .sr-live { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } /* The standing exit as a card: present with full anatomy, never dressed as a contender. Graphite instead of kinpaku, and it never takes the lead ring. */ .card.canon .face { border-color: var(--ks-rule); background: var(--ks-graphite); } @@ -776,6 +811,25 @@ function page() { b.closest('.card').classList.toggle('flipped'); })); + // Raise cycler: click (or Enter) advances to the next donation. + document.querySelectorAll('.raises-cycle').forEach(cycle => { + const raises = [...cycle.querySelectorAll('.raise')]; + const count = cycle.querySelector('[data-raises-count]'); + let at = 0; + const live = cycle.querySelector('.sr-live'); + const show = (announce) => { + raises.forEach((raise, i) => raise.classList.toggle('active', i === at)); + if (count) count.textContent = (at + 1) + '/' + raises.length; + // Screen readers hear the raise they just advanced to; the initial + // render stays quiet so page load does not narrate every card. + if (announce && live) live.textContent = 'Raise ' + (at + 1) + ' of ' + raises.length + ': ' + (raises[at]?.textContent || ''); + }; + show(false); + const advance = (e) => { e.stopPropagation(); at = (at + 1) % raises.length; show(true); }; + cycle.addEventListener('click', advance); + cycle.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); advance(e); } }); + }); + // Deal from the stack: cards begin piled at the grid's center, blurred, // then travel to their seats with a stagger. const cards = [...document.querySelectorAll('.card')]; @@ -823,7 +877,7 @@ function page() { const note = m.querySelector('.sketch-note'); const started = Date.now(); // A live elapsed count is the difference between "working" and "frozen". - const tick = setInterval(() => { if (note) note.textContent = 'sketching · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); + const tick = setInterval(() => { if (note) note.textContent = 'rendering · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); const settle = () => { clearInterval(tick); m.classList.remove('sketching', 'stand-in'); m.querySelector('.shimmer')?.remove(); m.querySelector('.stand-in-label')?.remove(); }; const standIn = () => { const pip = m.querySelector('.pip img'); @@ -834,7 +888,7 @@ function page() { clearInterval(tick); const label = document.createElement('p'); label.className = 'stand-in-label'; - label.textContent = 'inspiration · sketch pending'; + label.textContent = 'inspiration · comp pending'; m.appendChild(label); }; const tryLoad = () => { diff --git a/.opencode/skills/impeccable/reference/degraded/asset-producer.md b/.opencode/skills/impeccable/reference/degraded/asset-producer.md index bac780588..47e2bc31c 100644 --- a/.opencode/skills/impeccable/reference/degraded/asset-producer.md +++ b/.opencode/skills/impeccable/reference/degraded/asset-producer.md @@ -11,9 +11,9 @@ Your job is production cleanup, not new art direction. Work only from the approv Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster. -## Decision Sketches +## Decision Comps -When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame, including its aspect: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. The frame is shared across siblings so no sketch looks more finished than another; a finish gap breaks the comparison. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a sketch run. +When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `sketch` path (the field keeps its wire name) the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a comp is reported back, not padded from imagination. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (its regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment is what keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run. ## Input Contract diff --git a/.opencode/skills/impeccable/reference/degraded/finish-reviewer.md b/.opencode/skills/impeccable/reference/degraded/finish-reviewer.md index c49acadb0..c7bb7ca85 100644 --- a/.opencode/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.opencode/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,12 +11,12 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path; and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order -1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comps with no recorded pick mean the approval point was skipped, and that is a material finding. -2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. +1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comp-round comps with no recorded pick mean the approval point was skipped, and that is a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and they imply no approval whatever the build path; a code-led build has no comp round at all. +2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. A critique-reference comp, when one arrived on such a build, is provocation rather than spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is the question of what the image dared that the build did not, and the dares worth adopting enter material_fixes as ordinary ordered fixes. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. 3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition. 4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport. 5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material. diff --git a/.opencode/skills/impeccable/reference/new-work.md b/.opencode/skills/impeccable/reference/new-work.md index 95aa35654..bd3acf880 100644 --- a/.opencode/skills/impeccable/reference/new-work.md +++ b/.opencode/skills/impeccable/reference/new-work.md @@ -48,7 +48,7 @@ The script assigns which structure gets built; your top-ranked structure is what The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .opencode/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. +When image generation exists, every card also declares a `sketch` path under `.impeccable/mocks/decision/` (the field keeps its wire name for compatibility; what it carries is the card's comp), the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the comps; the page shimmer-waits per slot and the user may answer before they land. Each card's image is that direction's north-star comp at full fidelity, produced under the comp discipline in [visualize.md](visualize.md): the requested surface's first viewport, structure-led prompt, real product name and real content, no invented commercial claims, in that card's own palette, type character, and material world, committed all the way. Generation takes the same time at any fidelity, so an unfinished sketch pays sketch quality for comp cost; fairness between cards comes from equal fidelity in each card's own grammar, one surface, one aspect, never from shared unfinishedness. The frame's aspect is the surface's own: a native app or mobile-first surface comps portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen comped landscape is a broken frame, not a neutral default. Produce in the order the user reads, the assigned card, then the pick, then the full-card hand, then canon, each file written with its prompt sidecar the moment it is done, so a re-roll's spend front-loads onto the cards read first; declined challengers get no comp, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-comp packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. The chosen card's comp is not spent by the choice: on a comp-led build it enters the comp round as compositional option one, and on a code-led build it returns at the finish review as the critique reference, what the image dared that the build did not. The unchosen comps stay in `.impeccable/mocks/decision/` as the round's spent hand; they carry no approval and imply none. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. @@ -80,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. @@ -105,6 +105,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .opencode/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths, and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .opencode/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.opencode/skills/impeccable/reference/visualize.md b/.opencode/skills/impeccable/reference/visualize.md index 5bd14ae50..4948f5243 100644 --- a/.opencode/skills/impeccable/reference/visualize.md +++ b/.opencode/skills/impeccable/reference/visualize.md @@ -6,7 +6,7 @@ The purpose of a probe is to test composition, narrative, hierarchy, density, fo ## 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. A decision-page sketch is not a probe: it chose the direction at deliberately unfinished fidelity, so the three comps render regardless, and the chosen card's sketch seeds at most one of them. +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. - 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. - When the user shortlisted multiple concepts, spread the three across them. @@ -22,7 +22,7 @@ Show the three together: in the harness when it can display images, otherwise on Do not begin code until the user approves a direction or explicitly delegates the choice. If they delegate, choose using the task brief, PRODUCT.md, and DESIGN.md, and state the evidence. Approval refines the task concept; it does not modify DESIGN.md. -This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build with generated comps and no recorded approval as carrying a material finding. +This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build whose comp round produced comps with no recorded approval as carrying a material finding; decision comps under `.impeccable/mocks/decision/` are the direction round's hand, not comp-round output, and imply no approval on their own. After approval, record the choice where tools can find it: the approved comp's path goes in the surface brief, and the approved comp's `.json` prompt sidecar gains `"approved": true` (every comp generated through `generate-image.mjs` has one; create it if a native tool didn't). The sidecar travels with the mocks folder, so the approval survives sessions and machines that never see the brief. Then summarize the composition and the parts of the comp that must not be literalized, return to new-work.md, record the direction contract from the approved surface concept, and build. diff --git a/.opencode/skills/impeccable/scripts/serve-question.mjs b/.opencode/skills/impeccable/scripts/serve-question.mjs index bbbbdf77e..788211920 100644 --- a/.opencode/skills/impeccable/scripts/serve-question.mjs +++ b/.opencode/skills/impeccable/scripts/serve-question.mjs @@ -42,10 +42,12 @@ * // raise lines under the identity row * "risk": "one line: the honest risk", // optional * "body": "fallback prose when the structured fields are absent", - * "sketch": ".impeccable/sketches/assigned.webp", // optional; may not exist - * // yet: the page shimmer-waits and polls the - * // slot until the file lands, so serve first - * // and generate after + * "sketch": ".impeccable/mocks/decision/assigned.webp", // optional; the card's + * // full-fidelity direction comp (the field + * // keeps the sketch era's wire name). May not + * // exist yet: the page shimmer-waits and + * // polls the slot until the file lands, so + * // serve first and generate after * "hero": "https://... or /abs/path.webp", // optional inspiration image; * // rides picture-in-picture when a sketch exists * "board": "https://... or /abs/path.webp" // optional secondary image @@ -147,7 +149,7 @@ function printAnswer(raw) { console.log("CHOSEN CARD: open the chosen world's board and hero images now, before any code. When your harness only reads files, or runs sandboxed, download them INTO the workspace and open the relative path; a sandboxed viewer rejects absolute paths outside it. They set the craft bar the build must reach."); } if (a.sketch) { - console.log('CHOSEN SKETCH: the decision sketch at that path may seed one comp probe; the comp round still renders its full set, because a sketch chose the direction, not the composition.'); + console.log('CHOSEN COMP: the decision comp at that path is compositional option one. On a comp-led build the comp round adds two variations beside it; on a code-led build it returns at the finish review as the critique reference. Never regenerate it from scratch.'); } if (a.optionId === 'canon') { console.log('CANON CHOSEN: the user picked the category standard on purpose. Ask once for two or three products this should sit alongside; their craft level becomes the quality bar. Execute the canon at full commitment, conventions embraced without irony or smuggled quirk.'); @@ -173,17 +175,17 @@ if (hasFlag('schema')) { title: 'Choose the visual world', question: 'The roll assigned Fillmore Handbill. Keep it, take an alternate, or re-roll.', options: [ - { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, - { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/sketches/model-pick.webp' }, - { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, - { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, + { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/mocks/decision/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, + { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', palette: ['#1f1c18', '#efe5d0', '#a33327'], materials: ['woodcut', 'rag paper'], viewport: 'One tall sheet, the newest release as today’s ballad.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/mocks/decision/model-pick.webp' }, + { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', palette: ['#0000c0', '#ffff00', '#00c000', '#ffffff'], materials: ['block mosaic', 'phosphor glow'], viewport: 'P100 index page, releases as numbered rows.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', risk: 'Reads retro-novelty when the grid is not strict.', sketch: '.impeccable/mocks/decision/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, + { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', palette: ['#101418', '#9fb4c0'], materials: ['film grain', 'backlit glass'], case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, ], reroll: { registers: ['safer', 'bolder'] }, canon: true, - canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', viewport: 'The arrangement a visitor expects, at full craft.', sketch: '.impeccable/sketches/canon.webp' }, + 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.', sketch: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. 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. Sketch 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/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY 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 sketch slot carries the card’s full-fidelity direction comp (the field keeps its wire name for compatibility). 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); } @@ -337,16 +339,19 @@ function loadRound(json) { sketchSrc: sketchSrc(option.sketch), }); options = parsed.options.map(decorate); - // The verdict routes rendering: full cards first, declined cards demoted to - // the end of the deck in their own payload order. The reorder happens here - // so a payload that interleaves them still renders the weighing's shape. + // The verdict routes rendering: full cards first, then the canon, then the + // declined cards dead last in their own payload order. The reorder happens + // here so a payload that interleaves them still renders the weighing's + // shape, and the deck reads as a gradient of standing: contenders, the + // familiar door, then the demoted row. const declined = options.filter((o) => o.verdict === 'declined'); - if (declined.length) options = [...options.filter((o) => o.verdict !== 'declined'), ...declined]; + options = options.filter((o) => o.verdict !== 'declined'); // The standing exit as a full card: same anatomy, reserved id, rendered // subordinate by the page. Without it, canon stays the quiet footer action. if (parsed.canonCard && typeof parsed.canonCard === 'object') { options = [...options, { ...decorate(parsed.canonCard), id: 'canon', isCanon: true }]; } + options = [...options, ...declined]; } try { loadRound(raw); } catch (error) { console.error(`serve-question: ${error.message}`); process.exit(1); } const detachedKey = hasFlag('detached-serve') ? arg('key') : null; @@ -392,10 +397,21 @@ function page() { if (idBits.length) rows.push(`
${idBits.join('')}
`); // Donations from declined challengers render as named raise lines: the // assigned card arrives already raised by the hand it beat, and the raise - // is readable, because a raise nobody can read did not happen. + // is readable, because a raise nobody can read did not happen. One raise + // renders inline; several become a compact cycler (click advances), so a + // generous hand cannot blow the card out of proportion. if (Array.isArray(option.raised) && option.raised.length) { const nameOf = (id) => options.find((o) => o.id === id)?.label || String(id ?? ''); - rows.push(`
${option.raised.slice(0, 4).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`).join('')}
`); + const raiseLines = option.raised.slice(0, 6).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`); + if (raiseLines.length > 1) { + rows.push(`
+
Raised by the hand1/${raiseLines.length}
+ ${raiseLines.join('')} + +
`); + } else { + rows.push(`
${raiseLines[0]}
`); + } } // Demoted art stays reachable as a labeled thumb: the catalog world // explains where the direction comes from without buying it back the @@ -438,7 +454,7 @@ function page() { if (thumbOnly(option)) return ''; if (faceSketch(option)) { return `
-
sketching…
+
rendering…
${inspiration}
${expandChip}${details}
@@ -568,6 +584,10 @@ function page() { .nav.next { right: auto; left: 50%; top: auto; bottom: 6px; transform: translate(-50%, 0); } .fade-prev { top: 0; left: 0; right: 0; bottom: auto; width: auto; height: 72px; background: linear-gradient(180deg, var(--ks-lacquer), transparent); } .fade-next { top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 72px; background: linear-gradient(0deg, var(--ks-lacquer), transparent); } + /* In the vertical deck the cross axis is horizontal: flex-start would + shrink a declined card to content WIDTH, not height, so it stretches + like every other card and its height is already its own. */ + .grid > .card.declined { align-self: stretch; } } .card { position: relative; perspective: 1400px; transform: rotate(var(--fan, 0deg)); transition: transform .25s cubic-bezier(.16, 1, .3, 1); } .card:hover { transform: rotate(0deg) translateY(-4px); } @@ -660,7 +680,10 @@ function page() { and quieter, its catalog art rides as a labeled thumb in the body, and the action reads "Adopt anyway". Adoptable, never deleted: the demoted row is the hand's proof of judgment. */ - .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); } + /* Narrow AND short: without align-self the stretch default drags a thin + declined card to the tallest contender's height, a strange stilt of a + card beside the full hand. */ + .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); align-self: flex-start; } .card.declined .face { background: var(--ks-graphite); } .card.declined:hover .face { border-color: var(--ks-text-faint); } .card.declined h2 { font-size: 1rem; color: var(--ks-text); } @@ -677,6 +700,18 @@ function page() { .raises { display: flex; flex-direction: column; gap: 4px; margin: 2px 0; } .raise { font-size: .78rem; color: var(--ks-text-muted); line-height: 1.45; border-left: 2px solid var(--ks-patina); padding-left: 8px; } .raise .fact-label { color: var(--ks-patina); } + /* Several raises cycle instead of stacking: one visible at a time, a + counter for the rest, the whole block advances on click. */ + .raises-cycle { cursor: pointer; border-radius: 6px; } + .raises-cycle .raise { display: none; border-left: none; padding-left: 0; } + .raises-cycle .raise.active { display: block; } + .raises-cycle { border-left: 2px solid var(--ks-patina); padding-left: 8px; } + .raises-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; } + .raises-head .fact-label { color: var(--ks-patina); } + .raises-count { font-family: var(--ks-mono); font-size: .58rem; letter-spacing: .14em; color: var(--ks-text-faint); } + .raises-count::after { content: " \\203A"; } + .raises-cycle:hover .raises-count { color: var(--ks-patina); } + .sr-live { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } /* The standing exit as a card: present with full anatomy, never dressed as a contender. Graphite instead of kinpaku, and it never takes the lead ring. */ .card.canon .face { border-color: var(--ks-rule); background: var(--ks-graphite); } @@ -776,6 +811,25 @@ function page() { b.closest('.card').classList.toggle('flipped'); })); + // Raise cycler: click (or Enter) advances to the next donation. + document.querySelectorAll('.raises-cycle').forEach(cycle => { + const raises = [...cycle.querySelectorAll('.raise')]; + const count = cycle.querySelector('[data-raises-count]'); + let at = 0; + const live = cycle.querySelector('.sr-live'); + const show = (announce) => { + raises.forEach((raise, i) => raise.classList.toggle('active', i === at)); + if (count) count.textContent = (at + 1) + '/' + raises.length; + // Screen readers hear the raise they just advanced to; the initial + // render stays quiet so page load does not narrate every card. + if (announce && live) live.textContent = 'Raise ' + (at + 1) + ' of ' + raises.length + ': ' + (raises[at]?.textContent || ''); + }; + show(false); + const advance = (e) => { e.stopPropagation(); at = (at + 1) % raises.length; show(true); }; + cycle.addEventListener('click', advance); + cycle.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); advance(e); } }); + }); + // Deal from the stack: cards begin piled at the grid's center, blurred, // then travel to their seats with a stagger. const cards = [...document.querySelectorAll('.card')]; @@ -823,7 +877,7 @@ function page() { const note = m.querySelector('.sketch-note'); const started = Date.now(); // A live elapsed count is the difference between "working" and "frozen". - const tick = setInterval(() => { if (note) note.textContent = 'sketching · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); + const tick = setInterval(() => { if (note) note.textContent = 'rendering · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); const settle = () => { clearInterval(tick); m.classList.remove('sketching', 'stand-in'); m.querySelector('.shimmer')?.remove(); m.querySelector('.stand-in-label')?.remove(); }; const standIn = () => { const pip = m.querySelector('.pip img'); @@ -834,7 +888,7 @@ function page() { clearInterval(tick); const label = document.createElement('p'); label.className = 'stand-in-label'; - label.textContent = 'inspiration · sketch pending'; + label.textContent = 'inspiration · comp pending'; m.appendChild(label); }; const tryLoad = () => { diff --git a/.pi/skills/impeccable/reference/degraded/asset-producer.md b/.pi/skills/impeccable/reference/degraded/asset-producer.md index 393915eb6..619417a41 100644 --- a/.pi/skills/impeccable/reference/degraded/asset-producer.md +++ b/.pi/skills/impeccable/reference/degraded/asset-producer.md @@ -11,9 +11,9 @@ Your job is production cleanup, not new art direction. Work only from the approv Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster. -## Decision Sketches +## Decision Comps -When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame, including its aspect: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. The frame is shared across siblings so no sketch looks more finished than another; a finish gap breaks the comparison. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a sketch run. +When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `sketch` path (the field keeps its wire name) the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a comp is reported back, not padded from imagination. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (its regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment is what keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run. ## Input Contract diff --git a/.pi/skills/impeccable/reference/degraded/finish-reviewer.md b/.pi/skills/impeccable/reference/degraded/finish-reviewer.md index c49acadb0..c7bb7ca85 100644 --- a/.pi/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.pi/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,12 +11,12 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path; and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order -1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comps with no recorded pick mean the approval point was skipped, and that is a material finding. -2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. +1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comp-round comps with no recorded pick mean the approval point was skipped, and that is a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and they imply no approval whatever the build path; a code-led build has no comp round at all. +2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. A critique-reference comp, when one arrived on such a build, is provocation rather than spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is the question of what the image dared that the build did not, and the dares worth adopting enter material_fixes as ordinary ordered fixes. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. 3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition. 4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport. 5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material. diff --git a/.pi/skills/impeccable/reference/new-work.md b/.pi/skills/impeccable/reference/new-work.md index ffc87a930..405193039 100644 --- a/.pi/skills/impeccable/reference/new-work.md +++ b/.pi/skills/impeccable/reference/new-work.md @@ -48,7 +48,7 @@ The script assigns which structure gets built; your top-ranked structure is what The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .pi/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. +When image generation exists, every card also declares a `sketch` path under `.impeccable/mocks/decision/` (the field keeps its wire name for compatibility; what it carries is the card's comp), the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the comps; the page shimmer-waits per slot and the user may answer before they land. Each card's image is that direction's north-star comp at full fidelity, produced under the comp discipline in [visualize.md](visualize.md): the requested surface's first viewport, structure-led prompt, real product name and real content, no invented commercial claims, in that card's own palette, type character, and material world, committed all the way. Generation takes the same time at any fidelity, so an unfinished sketch pays sketch quality for comp cost; fairness between cards comes from equal fidelity in each card's own grammar, one surface, one aspect, never from shared unfinishedness. The frame's aspect is the surface's own: a native app or mobile-first surface comps portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen comped landscape is a broken frame, not a neutral default. Produce in the order the user reads, the assigned card, then the pick, then the full-card hand, then canon, each file written with its prompt sidecar the moment it is done, so a re-roll's spend front-loads onto the cards read first; declined challengers get no comp, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-comp packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. The chosen card's comp is not spent by the choice: on a comp-led build it enters the comp round as compositional option one, and on a code-led build it returns at the finish review as the critique reference, what the image dared that the build did not. The unchosen comps stay in `.impeccable/mocks/decision/` as the round's spent hand; they carry no approval and imply none. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. @@ -80,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. @@ -105,6 +105,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .pi/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths, and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .pi/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.pi/skills/impeccable/reference/visualize.md b/.pi/skills/impeccable/reference/visualize.md index 539dd0286..bf38bc747 100644 --- a/.pi/skills/impeccable/reference/visualize.md +++ b/.pi/skills/impeccable/reference/visualize.md @@ -6,7 +6,7 @@ The purpose of a probe is to test composition, narrative, hierarchy, density, fo ## 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. A decision-page sketch is not a probe: it chose the direction at deliberately unfinished fidelity, so the three comps render regardless, and the chosen card's sketch seeds at most one of them. +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. - 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. - When the user shortlisted multiple concepts, spread the three across them. @@ -22,7 +22,7 @@ Show the three together: in the harness when it can display images, otherwise on Do not begin code until the user approves a direction or explicitly delegates the choice. If they delegate, choose using the task brief, PRODUCT.md, and DESIGN.md, and state the evidence. Approval refines the task concept; it does not modify DESIGN.md. -This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build with generated comps and no recorded approval as carrying a material finding. +This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build whose comp round produced comps with no recorded approval as carrying a material finding; decision comps under `.impeccable/mocks/decision/` are the direction round's hand, not comp-round output, and imply no approval on their own. After approval, record the choice where tools can find it: the approved comp's path goes in the surface brief, and the approved comp's `.json` prompt sidecar gains `"approved": true` (every comp generated through `generate-image.mjs` has one; create it if a native tool didn't). The sidecar travels with the mocks folder, so the approval survives sessions and machines that never see the brief. Then summarize the composition and the parts of the comp that must not be literalized, return to new-work.md, record the direction contract from the approved surface concept, and build. diff --git a/.pi/skills/impeccable/scripts/serve-question.mjs b/.pi/skills/impeccable/scripts/serve-question.mjs index bbbbdf77e..788211920 100644 --- a/.pi/skills/impeccable/scripts/serve-question.mjs +++ b/.pi/skills/impeccable/scripts/serve-question.mjs @@ -42,10 +42,12 @@ * // raise lines under the identity row * "risk": "one line: the honest risk", // optional * "body": "fallback prose when the structured fields are absent", - * "sketch": ".impeccable/sketches/assigned.webp", // optional; may not exist - * // yet: the page shimmer-waits and polls the - * // slot until the file lands, so serve first - * // and generate after + * "sketch": ".impeccable/mocks/decision/assigned.webp", // optional; the card's + * // full-fidelity direction comp (the field + * // keeps the sketch era's wire name). May not + * // exist yet: the page shimmer-waits and + * // polls the slot until the file lands, so + * // serve first and generate after * "hero": "https://... or /abs/path.webp", // optional inspiration image; * // rides picture-in-picture when a sketch exists * "board": "https://... or /abs/path.webp" // optional secondary image @@ -147,7 +149,7 @@ function printAnswer(raw) { console.log("CHOSEN CARD: open the chosen world's board and hero images now, before any code. When your harness only reads files, or runs sandboxed, download them INTO the workspace and open the relative path; a sandboxed viewer rejects absolute paths outside it. They set the craft bar the build must reach."); } if (a.sketch) { - console.log('CHOSEN SKETCH: the decision sketch at that path may seed one comp probe; the comp round still renders its full set, because a sketch chose the direction, not the composition.'); + console.log('CHOSEN COMP: the decision comp at that path is compositional option one. On a comp-led build the comp round adds two variations beside it; on a code-led build it returns at the finish review as the critique reference. Never regenerate it from scratch.'); } if (a.optionId === 'canon') { console.log('CANON CHOSEN: the user picked the category standard on purpose. Ask once for two or three products this should sit alongside; their craft level becomes the quality bar. Execute the canon at full commitment, conventions embraced without irony or smuggled quirk.'); @@ -173,17 +175,17 @@ if (hasFlag('schema')) { title: 'Choose the visual world', question: 'The roll assigned Fillmore Handbill. Keep it, take an alternate, or re-roll.', options: [ - { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, - { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/sketches/model-pick.webp' }, - { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, - { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, + { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/mocks/decision/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, + { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', palette: ['#1f1c18', '#efe5d0', '#a33327'], materials: ['woodcut', 'rag paper'], viewport: 'One tall sheet, the newest release as today’s ballad.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/mocks/decision/model-pick.webp' }, + { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', palette: ['#0000c0', '#ffff00', '#00c000', '#ffffff'], materials: ['block mosaic', 'phosphor glow'], viewport: 'P100 index page, releases as numbered rows.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', risk: 'Reads retro-novelty when the grid is not strict.', sketch: '.impeccable/mocks/decision/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, + { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', palette: ['#101418', '#9fb4c0'], materials: ['film grain', 'backlit glass'], case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, ], reroll: { registers: ['safer', 'bolder'] }, canon: true, - canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', viewport: 'The arrangement a visitor expects, at full craft.', sketch: '.impeccable/sketches/canon.webp' }, + 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.', sketch: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. 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. Sketch 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/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY 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 sketch slot carries the card’s full-fidelity direction comp (the field keeps its wire name for compatibility). 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); } @@ -337,16 +339,19 @@ function loadRound(json) { sketchSrc: sketchSrc(option.sketch), }); options = parsed.options.map(decorate); - // The verdict routes rendering: full cards first, declined cards demoted to - // the end of the deck in their own payload order. The reorder happens here - // so a payload that interleaves them still renders the weighing's shape. + // The verdict routes rendering: full cards first, then the canon, then the + // declined cards dead last in their own payload order. The reorder happens + // here so a payload that interleaves them still renders the weighing's + // shape, and the deck reads as a gradient of standing: contenders, the + // familiar door, then the demoted row. const declined = options.filter((o) => o.verdict === 'declined'); - if (declined.length) options = [...options.filter((o) => o.verdict !== 'declined'), ...declined]; + options = options.filter((o) => o.verdict !== 'declined'); // The standing exit as a full card: same anatomy, reserved id, rendered // subordinate by the page. Without it, canon stays the quiet footer action. if (parsed.canonCard && typeof parsed.canonCard === 'object') { options = [...options, { ...decorate(parsed.canonCard), id: 'canon', isCanon: true }]; } + options = [...options, ...declined]; } try { loadRound(raw); } catch (error) { console.error(`serve-question: ${error.message}`); process.exit(1); } const detachedKey = hasFlag('detached-serve') ? arg('key') : null; @@ -392,10 +397,21 @@ function page() { if (idBits.length) rows.push(`
${idBits.join('')}
`); // Donations from declined challengers render as named raise lines: the // assigned card arrives already raised by the hand it beat, and the raise - // is readable, because a raise nobody can read did not happen. + // is readable, because a raise nobody can read did not happen. One raise + // renders inline; several become a compact cycler (click advances), so a + // generous hand cannot blow the card out of proportion. if (Array.isArray(option.raised) && option.raised.length) { const nameOf = (id) => options.find((o) => o.id === id)?.label || String(id ?? ''); - rows.push(`
${option.raised.slice(0, 4).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`).join('')}
`); + const raiseLines = option.raised.slice(0, 6).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`); + if (raiseLines.length > 1) { + rows.push(`
+
Raised by the hand1/${raiseLines.length}
+ ${raiseLines.join('')} + +
`); + } else { + rows.push(`
${raiseLines[0]}
`); + } } // Demoted art stays reachable as a labeled thumb: the catalog world // explains where the direction comes from without buying it back the @@ -438,7 +454,7 @@ function page() { if (thumbOnly(option)) return ''; if (faceSketch(option)) { return `
-
sketching…
+
rendering…
${inspiration}
${expandChip}${details}
@@ -568,6 +584,10 @@ function page() { .nav.next { right: auto; left: 50%; top: auto; bottom: 6px; transform: translate(-50%, 0); } .fade-prev { top: 0; left: 0; right: 0; bottom: auto; width: auto; height: 72px; background: linear-gradient(180deg, var(--ks-lacquer), transparent); } .fade-next { top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 72px; background: linear-gradient(0deg, var(--ks-lacquer), transparent); } + /* In the vertical deck the cross axis is horizontal: flex-start would + shrink a declined card to content WIDTH, not height, so it stretches + like every other card and its height is already its own. */ + .grid > .card.declined { align-self: stretch; } } .card { position: relative; perspective: 1400px; transform: rotate(var(--fan, 0deg)); transition: transform .25s cubic-bezier(.16, 1, .3, 1); } .card:hover { transform: rotate(0deg) translateY(-4px); } @@ -660,7 +680,10 @@ function page() { and quieter, its catalog art rides as a labeled thumb in the body, and the action reads "Adopt anyway". Adoptable, never deleted: the demoted row is the hand's proof of judgment. */ - .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); } + /* Narrow AND short: without align-self the stretch default drags a thin + declined card to the tallest contender's height, a strange stilt of a + card beside the full hand. */ + .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); align-self: flex-start; } .card.declined .face { background: var(--ks-graphite); } .card.declined:hover .face { border-color: var(--ks-text-faint); } .card.declined h2 { font-size: 1rem; color: var(--ks-text); } @@ -677,6 +700,18 @@ function page() { .raises { display: flex; flex-direction: column; gap: 4px; margin: 2px 0; } .raise { font-size: .78rem; color: var(--ks-text-muted); line-height: 1.45; border-left: 2px solid var(--ks-patina); padding-left: 8px; } .raise .fact-label { color: var(--ks-patina); } + /* Several raises cycle instead of stacking: one visible at a time, a + counter for the rest, the whole block advances on click. */ + .raises-cycle { cursor: pointer; border-radius: 6px; } + .raises-cycle .raise { display: none; border-left: none; padding-left: 0; } + .raises-cycle .raise.active { display: block; } + .raises-cycle { border-left: 2px solid var(--ks-patina); padding-left: 8px; } + .raises-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; } + .raises-head .fact-label { color: var(--ks-patina); } + .raises-count { font-family: var(--ks-mono); font-size: .58rem; letter-spacing: .14em; color: var(--ks-text-faint); } + .raises-count::after { content: " \\203A"; } + .raises-cycle:hover .raises-count { color: var(--ks-patina); } + .sr-live { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } /* The standing exit as a card: present with full anatomy, never dressed as a contender. Graphite instead of kinpaku, and it never takes the lead ring. */ .card.canon .face { border-color: var(--ks-rule); background: var(--ks-graphite); } @@ -776,6 +811,25 @@ function page() { b.closest('.card').classList.toggle('flipped'); })); + // Raise cycler: click (or Enter) advances to the next donation. + document.querySelectorAll('.raises-cycle').forEach(cycle => { + const raises = [...cycle.querySelectorAll('.raise')]; + const count = cycle.querySelector('[data-raises-count]'); + let at = 0; + const live = cycle.querySelector('.sr-live'); + const show = (announce) => { + raises.forEach((raise, i) => raise.classList.toggle('active', i === at)); + if (count) count.textContent = (at + 1) + '/' + raises.length; + // Screen readers hear the raise they just advanced to; the initial + // render stays quiet so page load does not narrate every card. + if (announce && live) live.textContent = 'Raise ' + (at + 1) + ' of ' + raises.length + ': ' + (raises[at]?.textContent || ''); + }; + show(false); + const advance = (e) => { e.stopPropagation(); at = (at + 1) % raises.length; show(true); }; + cycle.addEventListener('click', advance); + cycle.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); advance(e); } }); + }); + // Deal from the stack: cards begin piled at the grid's center, blurred, // then travel to their seats with a stagger. const cards = [...document.querySelectorAll('.card')]; @@ -823,7 +877,7 @@ function page() { const note = m.querySelector('.sketch-note'); const started = Date.now(); // A live elapsed count is the difference between "working" and "frozen". - const tick = setInterval(() => { if (note) note.textContent = 'sketching · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); + const tick = setInterval(() => { if (note) note.textContent = 'rendering · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); const settle = () => { clearInterval(tick); m.classList.remove('sketching', 'stand-in'); m.querySelector('.shimmer')?.remove(); m.querySelector('.stand-in-label')?.remove(); }; const standIn = () => { const pip = m.querySelector('.pip img'); @@ -834,7 +888,7 @@ function page() { clearInterval(tick); const label = document.createElement('p'); label.className = 'stand-in-label'; - label.textContent = 'inspiration · sketch pending'; + label.textContent = 'inspiration · comp pending'; m.appendChild(label); }; const tryLoad = () => { diff --git a/.qoder/skills/impeccable/reference/degraded/asset-producer.md b/.qoder/skills/impeccable/reference/degraded/asset-producer.md index cd7516a88..620e27e53 100644 --- a/.qoder/skills/impeccable/reference/degraded/asset-producer.md +++ b/.qoder/skills/impeccable/reference/degraded/asset-producer.md @@ -11,9 +11,9 @@ Your job is production cleanup, not new art direction. Work only from the approv Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster. -## Decision Sketches +## Decision Comps -When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame, including its aspect: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. The frame is shared across siblings so no sketch looks more finished than another; a finish gap breaks the comparison. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a sketch run. +When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `sketch` path (the field keeps its wire name) the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a comp is reported back, not padded from imagination. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (its regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment is what keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run. ## Input Contract diff --git a/.qoder/skills/impeccable/reference/degraded/finish-reviewer.md b/.qoder/skills/impeccable/reference/degraded/finish-reviewer.md index c49acadb0..c7bb7ca85 100644 --- a/.qoder/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.qoder/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,12 +11,12 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path; and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order -1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comps with no recorded pick mean the approval point was skipped, and that is a material finding. -2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. +1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comp-round comps with no recorded pick mean the approval point was skipped, and that is a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and they imply no approval whatever the build path; a code-led build has no comp round at all. +2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. A critique-reference comp, when one arrived on such a build, is provocation rather than spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is the question of what the image dared that the build did not, and the dares worth adopting enter material_fixes as ordinary ordered fixes. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. 3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition. 4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport. 5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material. diff --git a/.qoder/skills/impeccable/reference/new-work.md b/.qoder/skills/impeccable/reference/new-work.md index c0ce92c06..4b0b74875 100644 --- a/.qoder/skills/impeccable/reference/new-work.md +++ b/.qoder/skills/impeccable/reference/new-work.md @@ -48,7 +48,7 @@ The script assigns which structure gets built; your top-ranked structure is what The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .qoder/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. +When image generation exists, every card also declares a `sketch` path under `.impeccable/mocks/decision/` (the field keeps its wire name for compatibility; what it carries is the card's comp), the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the comps; the page shimmer-waits per slot and the user may answer before they land. Each card's image is that direction's north-star comp at full fidelity, produced under the comp discipline in [visualize.md](visualize.md): the requested surface's first viewport, structure-led prompt, real product name and real content, no invented commercial claims, in that card's own palette, type character, and material world, committed all the way. Generation takes the same time at any fidelity, so an unfinished sketch pays sketch quality for comp cost; fairness between cards comes from equal fidelity in each card's own grammar, one surface, one aspect, never from shared unfinishedness. The frame's aspect is the surface's own: a native app or mobile-first surface comps portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen comped landscape is a broken frame, not a neutral default. Produce in the order the user reads, the assigned card, then the pick, then the full-card hand, then canon, each file written with its prompt sidecar the moment it is done, so a re-roll's spend front-loads onto the cards read first; declined challengers get no comp, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-comp packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. The chosen card's comp is not spent by the choice: on a comp-led build it enters the comp round as compositional option one, and on a code-led build it returns at the finish review as the critique reference, what the image dared that the build did not. The unchosen comps stay in `.impeccable/mocks/decision/` as the round's spent hand; they carry no approval and imply none. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. @@ -80,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. @@ -105,6 +105,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .qoder/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths, and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .qoder/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.qoder/skills/impeccable/reference/visualize.md b/.qoder/skills/impeccable/reference/visualize.md index 3e6a3a7e9..136a3cae9 100644 --- a/.qoder/skills/impeccable/reference/visualize.md +++ b/.qoder/skills/impeccable/reference/visualize.md @@ -6,7 +6,7 @@ The purpose of a probe is to test composition, narrative, hierarchy, density, fo ## 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. A decision-page sketch is not a probe: it chose the direction at deliberately unfinished fidelity, so the three comps render regardless, and the chosen card's sketch seeds at most one of them. +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. - 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. - When the user shortlisted multiple concepts, spread the three across them. @@ -22,7 +22,7 @@ Show the three together: in the harness when it can display images, otherwise on Do not begin code until the user approves a direction or explicitly delegates the choice. If they delegate, choose using the task brief, PRODUCT.md, and DESIGN.md, and state the evidence. Approval refines the task concept; it does not modify DESIGN.md. -This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build with generated comps and no recorded approval as carrying a material finding. +This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build whose comp round produced comps with no recorded approval as carrying a material finding; decision comps under `.impeccable/mocks/decision/` are the direction round's hand, not comp-round output, and imply no approval on their own. After approval, record the choice where tools can find it: the approved comp's path goes in the surface brief, and the approved comp's `.json` prompt sidecar gains `"approved": true` (every comp generated through `generate-image.mjs` has one; create it if a native tool didn't). The sidecar travels with the mocks folder, so the approval survives sessions and machines that never see the brief. Then summarize the composition and the parts of the comp that must not be literalized, return to new-work.md, record the direction contract from the approved surface concept, and build. diff --git a/.qoder/skills/impeccable/scripts/serve-question.mjs b/.qoder/skills/impeccable/scripts/serve-question.mjs index bbbbdf77e..788211920 100644 --- a/.qoder/skills/impeccable/scripts/serve-question.mjs +++ b/.qoder/skills/impeccable/scripts/serve-question.mjs @@ -42,10 +42,12 @@ * // raise lines under the identity row * "risk": "one line: the honest risk", // optional * "body": "fallback prose when the structured fields are absent", - * "sketch": ".impeccable/sketches/assigned.webp", // optional; may not exist - * // yet: the page shimmer-waits and polls the - * // slot until the file lands, so serve first - * // and generate after + * "sketch": ".impeccable/mocks/decision/assigned.webp", // optional; the card's + * // full-fidelity direction comp (the field + * // keeps the sketch era's wire name). May not + * // exist yet: the page shimmer-waits and + * // polls the slot until the file lands, so + * // serve first and generate after * "hero": "https://... or /abs/path.webp", // optional inspiration image; * // rides picture-in-picture when a sketch exists * "board": "https://... or /abs/path.webp" // optional secondary image @@ -147,7 +149,7 @@ function printAnswer(raw) { console.log("CHOSEN CARD: open the chosen world's board and hero images now, before any code. When your harness only reads files, or runs sandboxed, download them INTO the workspace and open the relative path; a sandboxed viewer rejects absolute paths outside it. They set the craft bar the build must reach."); } if (a.sketch) { - console.log('CHOSEN SKETCH: the decision sketch at that path may seed one comp probe; the comp round still renders its full set, because a sketch chose the direction, not the composition.'); + console.log('CHOSEN COMP: the decision comp at that path is compositional option one. On a comp-led build the comp round adds two variations beside it; on a code-led build it returns at the finish review as the critique reference. Never regenerate it from scratch.'); } if (a.optionId === 'canon') { console.log('CANON CHOSEN: the user picked the category standard on purpose. Ask once for two or three products this should sit alongside; their craft level becomes the quality bar. Execute the canon at full commitment, conventions embraced without irony or smuggled quirk.'); @@ -173,17 +175,17 @@ if (hasFlag('schema')) { title: 'Choose the visual world', question: 'The roll assigned Fillmore Handbill. Keep it, take an alternate, or re-roll.', options: [ - { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, - { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/sketches/model-pick.webp' }, - { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, - { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, + { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/mocks/decision/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, + { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', palette: ['#1f1c18', '#efe5d0', '#a33327'], materials: ['woodcut', 'rag paper'], viewport: 'One tall sheet, the newest release as today’s ballad.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/mocks/decision/model-pick.webp' }, + { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', palette: ['#0000c0', '#ffff00', '#00c000', '#ffffff'], materials: ['block mosaic', 'phosphor glow'], viewport: 'P100 index page, releases as numbered rows.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', risk: 'Reads retro-novelty when the grid is not strict.', sketch: '.impeccable/mocks/decision/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, + { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', palette: ['#101418', '#9fb4c0'], materials: ['film grain', 'backlit glass'], case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, ], reroll: { registers: ['safer', 'bolder'] }, canon: true, - canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', viewport: 'The arrangement a visitor expects, at full craft.', sketch: '.impeccable/sketches/canon.webp' }, + 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.', sketch: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. 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. Sketch 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/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY 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 sketch slot carries the card’s full-fidelity direction comp (the field keeps its wire name for compatibility). 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); } @@ -337,16 +339,19 @@ function loadRound(json) { sketchSrc: sketchSrc(option.sketch), }); options = parsed.options.map(decorate); - // The verdict routes rendering: full cards first, declined cards demoted to - // the end of the deck in their own payload order. The reorder happens here - // so a payload that interleaves them still renders the weighing's shape. + // The verdict routes rendering: full cards first, then the canon, then the + // declined cards dead last in their own payload order. The reorder happens + // here so a payload that interleaves them still renders the weighing's + // shape, and the deck reads as a gradient of standing: contenders, the + // familiar door, then the demoted row. const declined = options.filter((o) => o.verdict === 'declined'); - if (declined.length) options = [...options.filter((o) => o.verdict !== 'declined'), ...declined]; + options = options.filter((o) => o.verdict !== 'declined'); // The standing exit as a full card: same anatomy, reserved id, rendered // subordinate by the page. Without it, canon stays the quiet footer action. if (parsed.canonCard && typeof parsed.canonCard === 'object') { options = [...options, { ...decorate(parsed.canonCard), id: 'canon', isCanon: true }]; } + options = [...options, ...declined]; } try { loadRound(raw); } catch (error) { console.error(`serve-question: ${error.message}`); process.exit(1); } const detachedKey = hasFlag('detached-serve') ? arg('key') : null; @@ -392,10 +397,21 @@ function page() { if (idBits.length) rows.push(`
${idBits.join('')}
`); // Donations from declined challengers render as named raise lines: the // assigned card arrives already raised by the hand it beat, and the raise - // is readable, because a raise nobody can read did not happen. + // is readable, because a raise nobody can read did not happen. One raise + // renders inline; several become a compact cycler (click advances), so a + // generous hand cannot blow the card out of proportion. if (Array.isArray(option.raised) && option.raised.length) { const nameOf = (id) => options.find((o) => o.id === id)?.label || String(id ?? ''); - rows.push(`
${option.raised.slice(0, 4).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`).join('')}
`); + const raiseLines = option.raised.slice(0, 6).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`); + if (raiseLines.length > 1) { + rows.push(`
+
Raised by the hand1/${raiseLines.length}
+ ${raiseLines.join('')} + +
`); + } else { + rows.push(`
${raiseLines[0]}
`); + } } // Demoted art stays reachable as a labeled thumb: the catalog world // explains where the direction comes from without buying it back the @@ -438,7 +454,7 @@ function page() { if (thumbOnly(option)) return ''; if (faceSketch(option)) { return `
-
sketching…
+
rendering…
${inspiration}
${expandChip}${details}
@@ -568,6 +584,10 @@ function page() { .nav.next { right: auto; left: 50%; top: auto; bottom: 6px; transform: translate(-50%, 0); } .fade-prev { top: 0; left: 0; right: 0; bottom: auto; width: auto; height: 72px; background: linear-gradient(180deg, var(--ks-lacquer), transparent); } .fade-next { top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 72px; background: linear-gradient(0deg, var(--ks-lacquer), transparent); } + /* In the vertical deck the cross axis is horizontal: flex-start would + shrink a declined card to content WIDTH, not height, so it stretches + like every other card and its height is already its own. */ + .grid > .card.declined { align-self: stretch; } } .card { position: relative; perspective: 1400px; transform: rotate(var(--fan, 0deg)); transition: transform .25s cubic-bezier(.16, 1, .3, 1); } .card:hover { transform: rotate(0deg) translateY(-4px); } @@ -660,7 +680,10 @@ function page() { and quieter, its catalog art rides as a labeled thumb in the body, and the action reads "Adopt anyway". Adoptable, never deleted: the demoted row is the hand's proof of judgment. */ - .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); } + /* Narrow AND short: without align-self the stretch default drags a thin + declined card to the tallest contender's height, a strange stilt of a + card beside the full hand. */ + .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); align-self: flex-start; } .card.declined .face { background: var(--ks-graphite); } .card.declined:hover .face { border-color: var(--ks-text-faint); } .card.declined h2 { font-size: 1rem; color: var(--ks-text); } @@ -677,6 +700,18 @@ function page() { .raises { display: flex; flex-direction: column; gap: 4px; margin: 2px 0; } .raise { font-size: .78rem; color: var(--ks-text-muted); line-height: 1.45; border-left: 2px solid var(--ks-patina); padding-left: 8px; } .raise .fact-label { color: var(--ks-patina); } + /* Several raises cycle instead of stacking: one visible at a time, a + counter for the rest, the whole block advances on click. */ + .raises-cycle { cursor: pointer; border-radius: 6px; } + .raises-cycle .raise { display: none; border-left: none; padding-left: 0; } + .raises-cycle .raise.active { display: block; } + .raises-cycle { border-left: 2px solid var(--ks-patina); padding-left: 8px; } + .raises-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; } + .raises-head .fact-label { color: var(--ks-patina); } + .raises-count { font-family: var(--ks-mono); font-size: .58rem; letter-spacing: .14em; color: var(--ks-text-faint); } + .raises-count::after { content: " \\203A"; } + .raises-cycle:hover .raises-count { color: var(--ks-patina); } + .sr-live { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } /* The standing exit as a card: present with full anatomy, never dressed as a contender. Graphite instead of kinpaku, and it never takes the lead ring. */ .card.canon .face { border-color: var(--ks-rule); background: var(--ks-graphite); } @@ -776,6 +811,25 @@ function page() { b.closest('.card').classList.toggle('flipped'); })); + // Raise cycler: click (or Enter) advances to the next donation. + document.querySelectorAll('.raises-cycle').forEach(cycle => { + const raises = [...cycle.querySelectorAll('.raise')]; + const count = cycle.querySelector('[data-raises-count]'); + let at = 0; + const live = cycle.querySelector('.sr-live'); + const show = (announce) => { + raises.forEach((raise, i) => raise.classList.toggle('active', i === at)); + if (count) count.textContent = (at + 1) + '/' + raises.length; + // Screen readers hear the raise they just advanced to; the initial + // render stays quiet so page load does not narrate every card. + if (announce && live) live.textContent = 'Raise ' + (at + 1) + ' of ' + raises.length + ': ' + (raises[at]?.textContent || ''); + }; + show(false); + const advance = (e) => { e.stopPropagation(); at = (at + 1) % raises.length; show(true); }; + cycle.addEventListener('click', advance); + cycle.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); advance(e); } }); + }); + // Deal from the stack: cards begin piled at the grid's center, blurred, // then travel to their seats with a stagger. const cards = [...document.querySelectorAll('.card')]; @@ -823,7 +877,7 @@ function page() { const note = m.querySelector('.sketch-note'); const started = Date.now(); // A live elapsed count is the difference between "working" and "frozen". - const tick = setInterval(() => { if (note) note.textContent = 'sketching · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); + const tick = setInterval(() => { if (note) note.textContent = 'rendering · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); const settle = () => { clearInterval(tick); m.classList.remove('sketching', 'stand-in'); m.querySelector('.shimmer')?.remove(); m.querySelector('.stand-in-label')?.remove(); }; const standIn = () => { const pip = m.querySelector('.pip img'); @@ -834,7 +888,7 @@ function page() { clearInterval(tick); const label = document.createElement('p'); label.className = 'stand-in-label'; - label.textContent = 'inspiration · sketch pending'; + label.textContent = 'inspiration · comp pending'; m.appendChild(label); }; const tryLoad = () => { diff --git a/.rovodev/skills/impeccable/reference/degraded/asset-producer.md b/.rovodev/skills/impeccable/reference/degraded/asset-producer.md index d043914ac..49cfec79c 100644 --- a/.rovodev/skills/impeccable/reference/degraded/asset-producer.md +++ b/.rovodev/skills/impeccable/reference/degraded/asset-producer.md @@ -11,9 +11,9 @@ Your job is production cleanup, not new art direction. Work only from the approv Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster. -## Decision Sketches +## Decision Comps -When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame, including its aspect: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. The frame is shared across siblings so no sketch looks more finished than another; a finish gap breaks the comparison. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a sketch run. +When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `sketch` path (the field keeps its wire name) the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a comp is reported back, not padded from imagination. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (its regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment is what keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run. ## Input Contract diff --git a/.rovodev/skills/impeccable/reference/degraded/finish-reviewer.md b/.rovodev/skills/impeccable/reference/degraded/finish-reviewer.md index c49acadb0..c7bb7ca85 100644 --- a/.rovodev/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.rovodev/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,12 +11,12 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path; and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order -1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comps with no recorded pick mean the approval point was skipped, and that is a material finding. -2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. +1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comp-round comps with no recorded pick mean the approval point was skipped, and that is a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and they imply no approval whatever the build path; a code-led build has no comp round at all. +2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. A critique-reference comp, when one arrived on such a build, is provocation rather than spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is the question of what the image dared that the build did not, and the dares worth adopting enter material_fixes as ordinary ordered fixes. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. 3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition. 4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport. 5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material. diff --git a/.rovodev/skills/impeccable/reference/new-work.md b/.rovodev/skills/impeccable/reference/new-work.md index c4a7821fc..dc9465914 100644 --- a/.rovodev/skills/impeccable/reference/new-work.md +++ b/.rovodev/skills/impeccable/reference/new-work.md @@ -48,7 +48,7 @@ The script assigns which structure gets built; your top-ranked structure is what The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .rovodev/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. +When image generation exists, every card also declares a `sketch` path under `.impeccable/mocks/decision/` (the field keeps its wire name for compatibility; what it carries is the card's comp), the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the comps; the page shimmer-waits per slot and the user may answer before they land. Each card's image is that direction's north-star comp at full fidelity, produced under the comp discipline in [visualize.md](visualize.md): the requested surface's first viewport, structure-led prompt, real product name and real content, no invented commercial claims, in that card's own palette, type character, and material world, committed all the way. Generation takes the same time at any fidelity, so an unfinished sketch pays sketch quality for comp cost; fairness between cards comes from equal fidelity in each card's own grammar, one surface, one aspect, never from shared unfinishedness. The frame's aspect is the surface's own: a native app or mobile-first surface comps portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen comped landscape is a broken frame, not a neutral default. Produce in the order the user reads, the assigned card, then the pick, then the full-card hand, then canon, each file written with its prompt sidecar the moment it is done, so a re-roll's spend front-loads onto the cards read first; declined challengers get no comp, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-comp packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. The chosen card's comp is not spent by the choice: on a comp-led build it enters the comp round as compositional option one, and on a code-led build it returns at the finish review as the critique reference, what the image dared that the build did not. The unchosen comps stay in `.impeccable/mocks/decision/` as the round's spent hand; they carry no approval and imply none. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. @@ -80,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. @@ -105,6 +105,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .rovodev/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths, and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .rovodev/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.rovodev/skills/impeccable/reference/visualize.md b/.rovodev/skills/impeccable/reference/visualize.md index 665e8c71c..d7df668e0 100644 --- a/.rovodev/skills/impeccable/reference/visualize.md +++ b/.rovodev/skills/impeccable/reference/visualize.md @@ -6,7 +6,7 @@ The purpose of a probe is to test composition, narrative, hierarchy, density, fo ## 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. A decision-page sketch is not a probe: it chose the direction at deliberately unfinished fidelity, so the three comps render regardless, and the chosen card's sketch seeds at most one of them. +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. - 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. - When the user shortlisted multiple concepts, spread the three across them. @@ -22,7 +22,7 @@ Show the three together: in the harness when it can display images, otherwise on Do not begin code until the user approves a direction or explicitly delegates the choice. If they delegate, choose using the task brief, PRODUCT.md, and DESIGN.md, and state the evidence. Approval refines the task concept; it does not modify DESIGN.md. -This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build with generated comps and no recorded approval as carrying a material finding. +This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build whose comp round produced comps with no recorded approval as carrying a material finding; decision comps under `.impeccable/mocks/decision/` are the direction round's hand, not comp-round output, and imply no approval on their own. After approval, record the choice where tools can find it: the approved comp's path goes in the surface brief, and the approved comp's `.json` prompt sidecar gains `"approved": true` (every comp generated through `generate-image.mjs` has one; create it if a native tool didn't). The sidecar travels with the mocks folder, so the approval survives sessions and machines that never see the brief. Then summarize the composition and the parts of the comp that must not be literalized, return to new-work.md, record the direction contract from the approved surface concept, and build. diff --git a/.rovodev/skills/impeccable/scripts/serve-question.mjs b/.rovodev/skills/impeccable/scripts/serve-question.mjs index bbbbdf77e..788211920 100644 --- a/.rovodev/skills/impeccable/scripts/serve-question.mjs +++ b/.rovodev/skills/impeccable/scripts/serve-question.mjs @@ -42,10 +42,12 @@ * // raise lines under the identity row * "risk": "one line: the honest risk", // optional * "body": "fallback prose when the structured fields are absent", - * "sketch": ".impeccable/sketches/assigned.webp", // optional; may not exist - * // yet: the page shimmer-waits and polls the - * // slot until the file lands, so serve first - * // and generate after + * "sketch": ".impeccable/mocks/decision/assigned.webp", // optional; the card's + * // full-fidelity direction comp (the field + * // keeps the sketch era's wire name). May not + * // exist yet: the page shimmer-waits and + * // polls the slot until the file lands, so + * // serve first and generate after * "hero": "https://... or /abs/path.webp", // optional inspiration image; * // rides picture-in-picture when a sketch exists * "board": "https://... or /abs/path.webp" // optional secondary image @@ -147,7 +149,7 @@ function printAnswer(raw) { console.log("CHOSEN CARD: open the chosen world's board and hero images now, before any code. When your harness only reads files, or runs sandboxed, download them INTO the workspace and open the relative path; a sandboxed viewer rejects absolute paths outside it. They set the craft bar the build must reach."); } if (a.sketch) { - console.log('CHOSEN SKETCH: the decision sketch at that path may seed one comp probe; the comp round still renders its full set, because a sketch chose the direction, not the composition.'); + console.log('CHOSEN COMP: the decision comp at that path is compositional option one. On a comp-led build the comp round adds two variations beside it; on a code-led build it returns at the finish review as the critique reference. Never regenerate it from scratch.'); } if (a.optionId === 'canon') { console.log('CANON CHOSEN: the user picked the category standard on purpose. Ask once for two or three products this should sit alongside; their craft level becomes the quality bar. Execute the canon at full commitment, conventions embraced without irony or smuggled quirk.'); @@ -173,17 +175,17 @@ if (hasFlag('schema')) { title: 'Choose the visual world', question: 'The roll assigned Fillmore Handbill. Keep it, take an alternate, or re-roll.', options: [ - { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, - { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/sketches/model-pick.webp' }, - { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, - { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, + { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/mocks/decision/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, + { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', palette: ['#1f1c18', '#efe5d0', '#a33327'], materials: ['woodcut', 'rag paper'], viewport: 'One tall sheet, the newest release as today’s ballad.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/mocks/decision/model-pick.webp' }, + { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', palette: ['#0000c0', '#ffff00', '#00c000', '#ffffff'], materials: ['block mosaic', 'phosphor glow'], viewport: 'P100 index page, releases as numbered rows.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', risk: 'Reads retro-novelty when the grid is not strict.', sketch: '.impeccable/mocks/decision/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, + { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', palette: ['#101418', '#9fb4c0'], materials: ['film grain', 'backlit glass'], case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, ], reroll: { registers: ['safer', 'bolder'] }, canon: true, - canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', viewport: 'The arrangement a visitor expects, at full craft.', sketch: '.impeccable/sketches/canon.webp' }, + 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.', sketch: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. 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. Sketch 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/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY 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 sketch slot carries the card’s full-fidelity direction comp (the field keeps its wire name for compatibility). 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); } @@ -337,16 +339,19 @@ function loadRound(json) { sketchSrc: sketchSrc(option.sketch), }); options = parsed.options.map(decorate); - // The verdict routes rendering: full cards first, declined cards demoted to - // the end of the deck in their own payload order. The reorder happens here - // so a payload that interleaves them still renders the weighing's shape. + // The verdict routes rendering: full cards first, then the canon, then the + // declined cards dead last in their own payload order. The reorder happens + // here so a payload that interleaves them still renders the weighing's + // shape, and the deck reads as a gradient of standing: contenders, the + // familiar door, then the demoted row. const declined = options.filter((o) => o.verdict === 'declined'); - if (declined.length) options = [...options.filter((o) => o.verdict !== 'declined'), ...declined]; + options = options.filter((o) => o.verdict !== 'declined'); // The standing exit as a full card: same anatomy, reserved id, rendered // subordinate by the page. Without it, canon stays the quiet footer action. if (parsed.canonCard && typeof parsed.canonCard === 'object') { options = [...options, { ...decorate(parsed.canonCard), id: 'canon', isCanon: true }]; } + options = [...options, ...declined]; } try { loadRound(raw); } catch (error) { console.error(`serve-question: ${error.message}`); process.exit(1); } const detachedKey = hasFlag('detached-serve') ? arg('key') : null; @@ -392,10 +397,21 @@ function page() { if (idBits.length) rows.push(`
${idBits.join('')}
`); // Donations from declined challengers render as named raise lines: the // assigned card arrives already raised by the hand it beat, and the raise - // is readable, because a raise nobody can read did not happen. + // is readable, because a raise nobody can read did not happen. One raise + // renders inline; several become a compact cycler (click advances), so a + // generous hand cannot blow the card out of proportion. if (Array.isArray(option.raised) && option.raised.length) { const nameOf = (id) => options.find((o) => o.id === id)?.label || String(id ?? ''); - rows.push(`
${option.raised.slice(0, 4).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`).join('')}
`); + const raiseLines = option.raised.slice(0, 6).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`); + if (raiseLines.length > 1) { + rows.push(`
+
Raised by the hand1/${raiseLines.length}
+ ${raiseLines.join('')} + +
`); + } else { + rows.push(`
${raiseLines[0]}
`); + } } // Demoted art stays reachable as a labeled thumb: the catalog world // explains where the direction comes from without buying it back the @@ -438,7 +454,7 @@ function page() { if (thumbOnly(option)) return ''; if (faceSketch(option)) { return `
-
sketching…
+
rendering…
${inspiration}
${expandChip}${details}
@@ -568,6 +584,10 @@ function page() { .nav.next { right: auto; left: 50%; top: auto; bottom: 6px; transform: translate(-50%, 0); } .fade-prev { top: 0; left: 0; right: 0; bottom: auto; width: auto; height: 72px; background: linear-gradient(180deg, var(--ks-lacquer), transparent); } .fade-next { top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 72px; background: linear-gradient(0deg, var(--ks-lacquer), transparent); } + /* In the vertical deck the cross axis is horizontal: flex-start would + shrink a declined card to content WIDTH, not height, so it stretches + like every other card and its height is already its own. */ + .grid > .card.declined { align-self: stretch; } } .card { position: relative; perspective: 1400px; transform: rotate(var(--fan, 0deg)); transition: transform .25s cubic-bezier(.16, 1, .3, 1); } .card:hover { transform: rotate(0deg) translateY(-4px); } @@ -660,7 +680,10 @@ function page() { and quieter, its catalog art rides as a labeled thumb in the body, and the action reads "Adopt anyway". Adoptable, never deleted: the demoted row is the hand's proof of judgment. */ - .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); } + /* Narrow AND short: without align-self the stretch default drags a thin + declined card to the tallest contender's height, a strange stilt of a + card beside the full hand. */ + .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); align-self: flex-start; } .card.declined .face { background: var(--ks-graphite); } .card.declined:hover .face { border-color: var(--ks-text-faint); } .card.declined h2 { font-size: 1rem; color: var(--ks-text); } @@ -677,6 +700,18 @@ function page() { .raises { display: flex; flex-direction: column; gap: 4px; margin: 2px 0; } .raise { font-size: .78rem; color: var(--ks-text-muted); line-height: 1.45; border-left: 2px solid var(--ks-patina); padding-left: 8px; } .raise .fact-label { color: var(--ks-patina); } + /* Several raises cycle instead of stacking: one visible at a time, a + counter for the rest, the whole block advances on click. */ + .raises-cycle { cursor: pointer; border-radius: 6px; } + .raises-cycle .raise { display: none; border-left: none; padding-left: 0; } + .raises-cycle .raise.active { display: block; } + .raises-cycle { border-left: 2px solid var(--ks-patina); padding-left: 8px; } + .raises-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; } + .raises-head .fact-label { color: var(--ks-patina); } + .raises-count { font-family: var(--ks-mono); font-size: .58rem; letter-spacing: .14em; color: var(--ks-text-faint); } + .raises-count::after { content: " \\203A"; } + .raises-cycle:hover .raises-count { color: var(--ks-patina); } + .sr-live { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } /* The standing exit as a card: present with full anatomy, never dressed as a contender. Graphite instead of kinpaku, and it never takes the lead ring. */ .card.canon .face { border-color: var(--ks-rule); background: var(--ks-graphite); } @@ -776,6 +811,25 @@ function page() { b.closest('.card').classList.toggle('flipped'); })); + // Raise cycler: click (or Enter) advances to the next donation. + document.querySelectorAll('.raises-cycle').forEach(cycle => { + const raises = [...cycle.querySelectorAll('.raise')]; + const count = cycle.querySelector('[data-raises-count]'); + let at = 0; + const live = cycle.querySelector('.sr-live'); + const show = (announce) => { + raises.forEach((raise, i) => raise.classList.toggle('active', i === at)); + if (count) count.textContent = (at + 1) + '/' + raises.length; + // Screen readers hear the raise they just advanced to; the initial + // render stays quiet so page load does not narrate every card. + if (announce && live) live.textContent = 'Raise ' + (at + 1) + ' of ' + raises.length + ': ' + (raises[at]?.textContent || ''); + }; + show(false); + const advance = (e) => { e.stopPropagation(); at = (at + 1) % raises.length; show(true); }; + cycle.addEventListener('click', advance); + cycle.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); advance(e); } }); + }); + // Deal from the stack: cards begin piled at the grid's center, blurred, // then travel to their seats with a stagger. const cards = [...document.querySelectorAll('.card')]; @@ -823,7 +877,7 @@ function page() { const note = m.querySelector('.sketch-note'); const started = Date.now(); // A live elapsed count is the difference between "working" and "frozen". - const tick = setInterval(() => { if (note) note.textContent = 'sketching · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); + const tick = setInterval(() => { if (note) note.textContent = 'rendering · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); const settle = () => { clearInterval(tick); m.classList.remove('sketching', 'stand-in'); m.querySelector('.shimmer')?.remove(); m.querySelector('.stand-in-label')?.remove(); }; const standIn = () => { const pip = m.querySelector('.pip img'); @@ -834,7 +888,7 @@ function page() { clearInterval(tick); const label = document.createElement('p'); label.className = 'stand-in-label'; - label.textContent = 'inspiration · sketch pending'; + label.textContent = 'inspiration · comp pending'; m.appendChild(label); }; const tryLoad = () => { diff --git a/.trae-cn/skills/impeccable/reference/degraded/asset-producer.md b/.trae-cn/skills/impeccable/reference/degraded/asset-producer.md index 9d225196d..fa20e59d7 100644 --- a/.trae-cn/skills/impeccable/reference/degraded/asset-producer.md +++ b/.trae-cn/skills/impeccable/reference/degraded/asset-producer.md @@ -11,9 +11,9 @@ Your job is production cleanup, not new art direction. Work only from the approv Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster. -## Decision Sketches +## Decision Comps -When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame, including its aspect: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. The frame is shared across siblings so no sketch looks more finished than another; a finish gap breaks the comparison. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a sketch run. +When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `sketch` path (the field keeps its wire name) the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a comp is reported back, not padded from imagination. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (its regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment is what keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run. ## Input Contract diff --git a/.trae-cn/skills/impeccable/reference/degraded/finish-reviewer.md b/.trae-cn/skills/impeccable/reference/degraded/finish-reviewer.md index c49acadb0..c7bb7ca85 100644 --- a/.trae-cn/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.trae-cn/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,12 +11,12 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path; and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order -1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comps with no recorded pick mean the approval point was skipped, and that is a material finding. -2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. +1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comp-round comps with no recorded pick mean the approval point was skipped, and that is a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and they imply no approval whatever the build path; a code-led build has no comp round at all. +2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. A critique-reference comp, when one arrived on such a build, is provocation rather than spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is the question of what the image dared that the build did not, and the dares worth adopting enter material_fixes as ordinary ordered fixes. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. 3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition. 4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport. 5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material. diff --git a/.trae-cn/skills/impeccable/reference/new-work.md b/.trae-cn/skills/impeccable/reference/new-work.md index 529b63dbc..71a988a4f 100644 --- a/.trae-cn/skills/impeccable/reference/new-work.md +++ b/.trae-cn/skills/impeccable/reference/new-work.md @@ -48,7 +48,7 @@ The script assigns which structure gets built; your top-ranked structure is what The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .trae-cn/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. +When image generation exists, every card also declares a `sketch` path under `.impeccable/mocks/decision/` (the field keeps its wire name for compatibility; what it carries is the card's comp), the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the comps; the page shimmer-waits per slot and the user may answer before they land. Each card's image is that direction's north-star comp at full fidelity, produced under the comp discipline in [visualize.md](visualize.md): the requested surface's first viewport, structure-led prompt, real product name and real content, no invented commercial claims, in that card's own palette, type character, and material world, committed all the way. Generation takes the same time at any fidelity, so an unfinished sketch pays sketch quality for comp cost; fairness between cards comes from equal fidelity in each card's own grammar, one surface, one aspect, never from shared unfinishedness. The frame's aspect is the surface's own: a native app or mobile-first surface comps portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen comped landscape is a broken frame, not a neutral default. Produce in the order the user reads, the assigned card, then the pick, then the full-card hand, then canon, each file written with its prompt sidecar the moment it is done, so a re-roll's spend front-loads onto the cards read first; declined challengers get no comp, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-comp packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. The chosen card's comp is not spent by the choice: on a comp-led build it enters the comp round as compositional option one, and on a code-led build it returns at the finish review as the critique reference, what the image dared that the build did not. The unchosen comps stay in `.impeccable/mocks/decision/` as the round's spent hand; they carry no approval and imply none. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. @@ -80,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. @@ -105,6 +105,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .trae-cn/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths, and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .trae-cn/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.trae-cn/skills/impeccable/reference/visualize.md b/.trae-cn/skills/impeccable/reference/visualize.md index 6d39ce00a..0075cf715 100644 --- a/.trae-cn/skills/impeccable/reference/visualize.md +++ b/.trae-cn/skills/impeccable/reference/visualize.md @@ -6,7 +6,7 @@ The purpose of a probe is to test composition, narrative, hierarchy, density, fo ## 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. A decision-page sketch is not a probe: it chose the direction at deliberately unfinished fidelity, so the three comps render regardless, and the chosen card's sketch seeds at most one of them. +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. - 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. - When the user shortlisted multiple concepts, spread the three across them. @@ -22,7 +22,7 @@ Show the three together: in the harness when it can display images, otherwise on Do not begin code until the user approves a direction or explicitly delegates the choice. If they delegate, choose using the task brief, PRODUCT.md, and DESIGN.md, and state the evidence. Approval refines the task concept; it does not modify DESIGN.md. -This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build with generated comps and no recorded approval as carrying a material finding. +This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build whose comp round produced comps with no recorded approval as carrying a material finding; decision comps under `.impeccable/mocks/decision/` are the direction round's hand, not comp-round output, and imply no approval on their own. After approval, record the choice where tools can find it: the approved comp's path goes in the surface brief, and the approved comp's `.json` prompt sidecar gains `"approved": true` (every comp generated through `generate-image.mjs` has one; create it if a native tool didn't). The sidecar travels with the mocks folder, so the approval survives sessions and machines that never see the brief. Then summarize the composition and the parts of the comp that must not be literalized, return to new-work.md, record the direction contract from the approved surface concept, and build. diff --git a/.trae-cn/skills/impeccable/scripts/serve-question.mjs b/.trae-cn/skills/impeccable/scripts/serve-question.mjs index bbbbdf77e..788211920 100644 --- a/.trae-cn/skills/impeccable/scripts/serve-question.mjs +++ b/.trae-cn/skills/impeccable/scripts/serve-question.mjs @@ -42,10 +42,12 @@ * // raise lines under the identity row * "risk": "one line: the honest risk", // optional * "body": "fallback prose when the structured fields are absent", - * "sketch": ".impeccable/sketches/assigned.webp", // optional; may not exist - * // yet: the page shimmer-waits and polls the - * // slot until the file lands, so serve first - * // and generate after + * "sketch": ".impeccable/mocks/decision/assigned.webp", // optional; the card's + * // full-fidelity direction comp (the field + * // keeps the sketch era's wire name). May not + * // exist yet: the page shimmer-waits and + * // polls the slot until the file lands, so + * // serve first and generate after * "hero": "https://... or /abs/path.webp", // optional inspiration image; * // rides picture-in-picture when a sketch exists * "board": "https://... or /abs/path.webp" // optional secondary image @@ -147,7 +149,7 @@ function printAnswer(raw) { console.log("CHOSEN CARD: open the chosen world's board and hero images now, before any code. When your harness only reads files, or runs sandboxed, download them INTO the workspace and open the relative path; a sandboxed viewer rejects absolute paths outside it. They set the craft bar the build must reach."); } if (a.sketch) { - console.log('CHOSEN SKETCH: the decision sketch at that path may seed one comp probe; the comp round still renders its full set, because a sketch chose the direction, not the composition.'); + console.log('CHOSEN COMP: the decision comp at that path is compositional option one. On a comp-led build the comp round adds two variations beside it; on a code-led build it returns at the finish review as the critique reference. Never regenerate it from scratch.'); } if (a.optionId === 'canon') { console.log('CANON CHOSEN: the user picked the category standard on purpose. Ask once for two or three products this should sit alongside; their craft level becomes the quality bar. Execute the canon at full commitment, conventions embraced without irony or smuggled quirk.'); @@ -173,17 +175,17 @@ if (hasFlag('schema')) { title: 'Choose the visual world', question: 'The roll assigned Fillmore Handbill. Keep it, take an alternate, or re-roll.', options: [ - { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, - { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/sketches/model-pick.webp' }, - { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, - { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, + { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/mocks/decision/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, + { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', palette: ['#1f1c18', '#efe5d0', '#a33327'], materials: ['woodcut', 'rag paper'], viewport: 'One tall sheet, the newest release as today’s ballad.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/mocks/decision/model-pick.webp' }, + { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', palette: ['#0000c0', '#ffff00', '#00c000', '#ffffff'], materials: ['block mosaic', 'phosphor glow'], viewport: 'P100 index page, releases as numbered rows.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', risk: 'Reads retro-novelty when the grid is not strict.', sketch: '.impeccable/mocks/decision/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, + { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', palette: ['#101418', '#9fb4c0'], materials: ['film grain', 'backlit glass'], case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, ], reroll: { registers: ['safer', 'bolder'] }, canon: true, - canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', viewport: 'The arrangement a visitor expects, at full craft.', sketch: '.impeccable/sketches/canon.webp' }, + 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.', sketch: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. 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. Sketch 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/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY 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 sketch slot carries the card’s full-fidelity direction comp (the field keeps its wire name for compatibility). 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); } @@ -337,16 +339,19 @@ function loadRound(json) { sketchSrc: sketchSrc(option.sketch), }); options = parsed.options.map(decorate); - // The verdict routes rendering: full cards first, declined cards demoted to - // the end of the deck in their own payload order. The reorder happens here - // so a payload that interleaves them still renders the weighing's shape. + // The verdict routes rendering: full cards first, then the canon, then the + // declined cards dead last in their own payload order. The reorder happens + // here so a payload that interleaves them still renders the weighing's + // shape, and the deck reads as a gradient of standing: contenders, the + // familiar door, then the demoted row. const declined = options.filter((o) => o.verdict === 'declined'); - if (declined.length) options = [...options.filter((o) => o.verdict !== 'declined'), ...declined]; + options = options.filter((o) => o.verdict !== 'declined'); // The standing exit as a full card: same anatomy, reserved id, rendered // subordinate by the page. Without it, canon stays the quiet footer action. if (parsed.canonCard && typeof parsed.canonCard === 'object') { options = [...options, { ...decorate(parsed.canonCard), id: 'canon', isCanon: true }]; } + options = [...options, ...declined]; } try { loadRound(raw); } catch (error) { console.error(`serve-question: ${error.message}`); process.exit(1); } const detachedKey = hasFlag('detached-serve') ? arg('key') : null; @@ -392,10 +397,21 @@ function page() { if (idBits.length) rows.push(`
${idBits.join('')}
`); // Donations from declined challengers render as named raise lines: the // assigned card arrives already raised by the hand it beat, and the raise - // is readable, because a raise nobody can read did not happen. + // is readable, because a raise nobody can read did not happen. One raise + // renders inline; several become a compact cycler (click advances), so a + // generous hand cannot blow the card out of proportion. if (Array.isArray(option.raised) && option.raised.length) { const nameOf = (id) => options.find((o) => o.id === id)?.label || String(id ?? ''); - rows.push(`
${option.raised.slice(0, 4).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`).join('')}
`); + const raiseLines = option.raised.slice(0, 6).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`); + if (raiseLines.length > 1) { + rows.push(`
+
Raised by the hand1/${raiseLines.length}
+ ${raiseLines.join('')} + +
`); + } else { + rows.push(`
${raiseLines[0]}
`); + } } // Demoted art stays reachable as a labeled thumb: the catalog world // explains where the direction comes from without buying it back the @@ -438,7 +454,7 @@ function page() { if (thumbOnly(option)) return ''; if (faceSketch(option)) { return `
-
sketching…
+
rendering…
${inspiration}
${expandChip}${details}
@@ -568,6 +584,10 @@ function page() { .nav.next { right: auto; left: 50%; top: auto; bottom: 6px; transform: translate(-50%, 0); } .fade-prev { top: 0; left: 0; right: 0; bottom: auto; width: auto; height: 72px; background: linear-gradient(180deg, var(--ks-lacquer), transparent); } .fade-next { top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 72px; background: linear-gradient(0deg, var(--ks-lacquer), transparent); } + /* In the vertical deck the cross axis is horizontal: flex-start would + shrink a declined card to content WIDTH, not height, so it stretches + like every other card and its height is already its own. */ + .grid > .card.declined { align-self: stretch; } } .card { position: relative; perspective: 1400px; transform: rotate(var(--fan, 0deg)); transition: transform .25s cubic-bezier(.16, 1, .3, 1); } .card:hover { transform: rotate(0deg) translateY(-4px); } @@ -660,7 +680,10 @@ function page() { and quieter, its catalog art rides as a labeled thumb in the body, and the action reads "Adopt anyway". Adoptable, never deleted: the demoted row is the hand's proof of judgment. */ - .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); } + /* Narrow AND short: without align-self the stretch default drags a thin + declined card to the tallest contender's height, a strange stilt of a + card beside the full hand. */ + .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); align-self: flex-start; } .card.declined .face { background: var(--ks-graphite); } .card.declined:hover .face { border-color: var(--ks-text-faint); } .card.declined h2 { font-size: 1rem; color: var(--ks-text); } @@ -677,6 +700,18 @@ function page() { .raises { display: flex; flex-direction: column; gap: 4px; margin: 2px 0; } .raise { font-size: .78rem; color: var(--ks-text-muted); line-height: 1.45; border-left: 2px solid var(--ks-patina); padding-left: 8px; } .raise .fact-label { color: var(--ks-patina); } + /* Several raises cycle instead of stacking: one visible at a time, a + counter for the rest, the whole block advances on click. */ + .raises-cycle { cursor: pointer; border-radius: 6px; } + .raises-cycle .raise { display: none; border-left: none; padding-left: 0; } + .raises-cycle .raise.active { display: block; } + .raises-cycle { border-left: 2px solid var(--ks-patina); padding-left: 8px; } + .raises-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; } + .raises-head .fact-label { color: var(--ks-patina); } + .raises-count { font-family: var(--ks-mono); font-size: .58rem; letter-spacing: .14em; color: var(--ks-text-faint); } + .raises-count::after { content: " \\203A"; } + .raises-cycle:hover .raises-count { color: var(--ks-patina); } + .sr-live { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } /* The standing exit as a card: present with full anatomy, never dressed as a contender. Graphite instead of kinpaku, and it never takes the lead ring. */ .card.canon .face { border-color: var(--ks-rule); background: var(--ks-graphite); } @@ -776,6 +811,25 @@ function page() { b.closest('.card').classList.toggle('flipped'); })); + // Raise cycler: click (or Enter) advances to the next donation. + document.querySelectorAll('.raises-cycle').forEach(cycle => { + const raises = [...cycle.querySelectorAll('.raise')]; + const count = cycle.querySelector('[data-raises-count]'); + let at = 0; + const live = cycle.querySelector('.sr-live'); + const show = (announce) => { + raises.forEach((raise, i) => raise.classList.toggle('active', i === at)); + if (count) count.textContent = (at + 1) + '/' + raises.length; + // Screen readers hear the raise they just advanced to; the initial + // render stays quiet so page load does not narrate every card. + if (announce && live) live.textContent = 'Raise ' + (at + 1) + ' of ' + raises.length + ': ' + (raises[at]?.textContent || ''); + }; + show(false); + const advance = (e) => { e.stopPropagation(); at = (at + 1) % raises.length; show(true); }; + cycle.addEventListener('click', advance); + cycle.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); advance(e); } }); + }); + // Deal from the stack: cards begin piled at the grid's center, blurred, // then travel to their seats with a stagger. const cards = [...document.querySelectorAll('.card')]; @@ -823,7 +877,7 @@ function page() { const note = m.querySelector('.sketch-note'); const started = Date.now(); // A live elapsed count is the difference between "working" and "frozen". - const tick = setInterval(() => { if (note) note.textContent = 'sketching · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); + const tick = setInterval(() => { if (note) note.textContent = 'rendering · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); const settle = () => { clearInterval(tick); m.classList.remove('sketching', 'stand-in'); m.querySelector('.shimmer')?.remove(); m.querySelector('.stand-in-label')?.remove(); }; const standIn = () => { const pip = m.querySelector('.pip img'); @@ -834,7 +888,7 @@ function page() { clearInterval(tick); const label = document.createElement('p'); label.className = 'stand-in-label'; - label.textContent = 'inspiration · sketch pending'; + label.textContent = 'inspiration · comp pending'; m.appendChild(label); }; const tryLoad = () => { diff --git a/.trae/skills/impeccable/reference/degraded/asset-producer.md b/.trae/skills/impeccable/reference/degraded/asset-producer.md index dd68135d1..28ce0c72c 100644 --- a/.trae/skills/impeccable/reference/degraded/asset-producer.md +++ b/.trae/skills/impeccable/reference/degraded/asset-producer.md @@ -11,9 +11,9 @@ Your job is production cleanup, not new art direction. Work only from the approv Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster. -## Decision Sketches +## Decision Comps -When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame, including its aspect: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. The frame is shared across siblings so no sketch looks more finished than another; a finish gap breaks the comparison. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a sketch run. +When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `sketch` path (the field keeps its wire name) the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a comp is reported back, not padded from imagination. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (its regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment is what keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run. ## Input Contract diff --git a/.trae/skills/impeccable/reference/degraded/finish-reviewer.md b/.trae/skills/impeccable/reference/degraded/finish-reviewer.md index c49acadb0..c7bb7ca85 100644 --- a/.trae/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.trae/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,12 +11,12 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path; and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order -1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comps with no recorded pick mean the approval point was skipped, and that is a material finding. -2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. +1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comp-round comps with no recorded pick mean the approval point was skipped, and that is a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and they imply no approval whatever the build path; a code-led build has no comp round at all. +2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. A critique-reference comp, when one arrived on such a build, is provocation rather than spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is the question of what the image dared that the build did not, and the dares worth adopting enter material_fixes as ordinary ordered fixes. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. 3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition. 4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport. 5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material. diff --git a/.trae/skills/impeccable/reference/new-work.md b/.trae/skills/impeccable/reference/new-work.md index 5c805afc8..21db0a1b6 100644 --- a/.trae/skills/impeccable/reference/new-work.md +++ b/.trae/skills/impeccable/reference/new-work.md @@ -48,7 +48,7 @@ The script assigns which structure gets built; your top-ranked structure is what The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .trae/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. +When image generation exists, every card also declares a `sketch` path under `.impeccable/mocks/decision/` (the field keeps its wire name for compatibility; what it carries is the card's comp), the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the comps; the page shimmer-waits per slot and the user may answer before they land. Each card's image is that direction's north-star comp at full fidelity, produced under the comp discipline in [visualize.md](visualize.md): the requested surface's first viewport, structure-led prompt, real product name and real content, no invented commercial claims, in that card's own palette, type character, and material world, committed all the way. Generation takes the same time at any fidelity, so an unfinished sketch pays sketch quality for comp cost; fairness between cards comes from equal fidelity in each card's own grammar, one surface, one aspect, never from shared unfinishedness. The frame's aspect is the surface's own: a native app or mobile-first surface comps portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen comped landscape is a broken frame, not a neutral default. Produce in the order the user reads, the assigned card, then the pick, then the full-card hand, then canon, each file written with its prompt sidecar the moment it is done, so a re-roll's spend front-loads onto the cards read first; declined challengers get no comp, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-comp packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. The chosen card's comp is not spent by the choice: on a comp-led build it enters the comp round as compositional option one, and on a code-led build it returns at the finish review as the critique reference, what the image dared that the build did not. The unchosen comps stay in `.impeccable/mocks/decision/` as the round's spent hand; they carry no approval and imply none. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. @@ -80,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. @@ -105,6 +105,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .trae/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths, and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .trae/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.trae/skills/impeccable/reference/visualize.md b/.trae/skills/impeccable/reference/visualize.md index 247ea2067..096cc2e1d 100644 --- a/.trae/skills/impeccable/reference/visualize.md +++ b/.trae/skills/impeccable/reference/visualize.md @@ -6,7 +6,7 @@ The purpose of a probe is to test composition, narrative, hierarchy, density, fo ## 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. A decision-page sketch is not a probe: it chose the direction at deliberately unfinished fidelity, so the three comps render regardless, and the chosen card's sketch seeds at most one of them. +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. - 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. - When the user shortlisted multiple concepts, spread the three across them. @@ -22,7 +22,7 @@ Show the three together: in the harness when it can display images, otherwise on Do not begin code until the user approves a direction or explicitly delegates the choice. If they delegate, choose using the task brief, PRODUCT.md, and DESIGN.md, and state the evidence. Approval refines the task concept; it does not modify DESIGN.md. -This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build with generated comps and no recorded approval as carrying a material finding. +This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build whose comp round produced comps with no recorded approval as carrying a material finding; decision comps under `.impeccable/mocks/decision/` are the direction round's hand, not comp-round output, and imply no approval on their own. After approval, record the choice where tools can find it: the approved comp's path goes in the surface brief, and the approved comp's `.json` prompt sidecar gains `"approved": true` (every comp generated through `generate-image.mjs` has one; create it if a native tool didn't). The sidecar travels with the mocks folder, so the approval survives sessions and machines that never see the brief. Then summarize the composition and the parts of the comp that must not be literalized, return to new-work.md, record the direction contract from the approved surface concept, and build. diff --git a/.trae/skills/impeccable/scripts/serve-question.mjs b/.trae/skills/impeccable/scripts/serve-question.mjs index bbbbdf77e..788211920 100644 --- a/.trae/skills/impeccable/scripts/serve-question.mjs +++ b/.trae/skills/impeccable/scripts/serve-question.mjs @@ -42,10 +42,12 @@ * // raise lines under the identity row * "risk": "one line: the honest risk", // optional * "body": "fallback prose when the structured fields are absent", - * "sketch": ".impeccable/sketches/assigned.webp", // optional; may not exist - * // yet: the page shimmer-waits and polls the - * // slot until the file lands, so serve first - * // and generate after + * "sketch": ".impeccable/mocks/decision/assigned.webp", // optional; the card's + * // full-fidelity direction comp (the field + * // keeps the sketch era's wire name). May not + * // exist yet: the page shimmer-waits and + * // polls the slot until the file lands, so + * // serve first and generate after * "hero": "https://... or /abs/path.webp", // optional inspiration image; * // rides picture-in-picture when a sketch exists * "board": "https://... or /abs/path.webp" // optional secondary image @@ -147,7 +149,7 @@ function printAnswer(raw) { console.log("CHOSEN CARD: open the chosen world's board and hero images now, before any code. When your harness only reads files, or runs sandboxed, download them INTO the workspace and open the relative path; a sandboxed viewer rejects absolute paths outside it. They set the craft bar the build must reach."); } if (a.sketch) { - console.log('CHOSEN SKETCH: the decision sketch at that path may seed one comp probe; the comp round still renders its full set, because a sketch chose the direction, not the composition.'); + console.log('CHOSEN COMP: the decision comp at that path is compositional option one. On a comp-led build the comp round adds two variations beside it; on a code-led build it returns at the finish review as the critique reference. Never regenerate it from scratch.'); } if (a.optionId === 'canon') { console.log('CANON CHOSEN: the user picked the category standard on purpose. Ask once for two or three products this should sit alongside; their craft level becomes the quality bar. Execute the canon at full commitment, conventions embraced without irony or smuggled quirk.'); @@ -173,17 +175,17 @@ if (hasFlag('schema')) { title: 'Choose the visual world', question: 'The roll assigned Fillmore Handbill. Keep it, take an alternate, or re-roll.', options: [ - { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, - { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/sketches/model-pick.webp' }, - { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, - { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, + { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/mocks/decision/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, + { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', palette: ['#1f1c18', '#efe5d0', '#a33327'], materials: ['woodcut', 'rag paper'], viewport: 'One tall sheet, the newest release as today’s ballad.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/mocks/decision/model-pick.webp' }, + { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', palette: ['#0000c0', '#ffff00', '#00c000', '#ffffff'], materials: ['block mosaic', 'phosphor glow'], viewport: 'P100 index page, releases as numbered rows.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', risk: 'Reads retro-novelty when the grid is not strict.', sketch: '.impeccable/mocks/decision/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, + { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', palette: ['#101418', '#9fb4c0'], materials: ['film grain', 'backlit glass'], case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, ], reroll: { registers: ['safer', 'bolder'] }, canon: true, - canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', viewport: 'The arrangement a visitor expects, at full craft.', sketch: '.impeccable/sketches/canon.webp' }, + 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.', sketch: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. 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. Sketch 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/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY 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 sketch slot carries the card’s full-fidelity direction comp (the field keeps its wire name for compatibility). 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); } @@ -337,16 +339,19 @@ function loadRound(json) { sketchSrc: sketchSrc(option.sketch), }); options = parsed.options.map(decorate); - // The verdict routes rendering: full cards first, declined cards demoted to - // the end of the deck in their own payload order. The reorder happens here - // so a payload that interleaves them still renders the weighing's shape. + // The verdict routes rendering: full cards first, then the canon, then the + // declined cards dead last in their own payload order. The reorder happens + // here so a payload that interleaves them still renders the weighing's + // shape, and the deck reads as a gradient of standing: contenders, the + // familiar door, then the demoted row. const declined = options.filter((o) => o.verdict === 'declined'); - if (declined.length) options = [...options.filter((o) => o.verdict !== 'declined'), ...declined]; + options = options.filter((o) => o.verdict !== 'declined'); // The standing exit as a full card: same anatomy, reserved id, rendered // subordinate by the page. Without it, canon stays the quiet footer action. if (parsed.canonCard && typeof parsed.canonCard === 'object') { options = [...options, { ...decorate(parsed.canonCard), id: 'canon', isCanon: true }]; } + options = [...options, ...declined]; } try { loadRound(raw); } catch (error) { console.error(`serve-question: ${error.message}`); process.exit(1); } const detachedKey = hasFlag('detached-serve') ? arg('key') : null; @@ -392,10 +397,21 @@ function page() { if (idBits.length) rows.push(`
${idBits.join('')}
`); // Donations from declined challengers render as named raise lines: the // assigned card arrives already raised by the hand it beat, and the raise - // is readable, because a raise nobody can read did not happen. + // is readable, because a raise nobody can read did not happen. One raise + // renders inline; several become a compact cycler (click advances), so a + // generous hand cannot blow the card out of proportion. if (Array.isArray(option.raised) && option.raised.length) { const nameOf = (id) => options.find((o) => o.id === id)?.label || String(id ?? ''); - rows.push(`
${option.raised.slice(0, 4).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`).join('')}
`); + const raiseLines = option.raised.slice(0, 6).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`); + if (raiseLines.length > 1) { + rows.push(`
+
Raised by the hand1/${raiseLines.length}
+ ${raiseLines.join('')} + +
`); + } else { + rows.push(`
${raiseLines[0]}
`); + } } // Demoted art stays reachable as a labeled thumb: the catalog world // explains where the direction comes from without buying it back the @@ -438,7 +454,7 @@ function page() { if (thumbOnly(option)) return ''; if (faceSketch(option)) { return `
-
sketching…
+
rendering…
${inspiration}
${expandChip}${details}
@@ -568,6 +584,10 @@ function page() { .nav.next { right: auto; left: 50%; top: auto; bottom: 6px; transform: translate(-50%, 0); } .fade-prev { top: 0; left: 0; right: 0; bottom: auto; width: auto; height: 72px; background: linear-gradient(180deg, var(--ks-lacquer), transparent); } .fade-next { top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 72px; background: linear-gradient(0deg, var(--ks-lacquer), transparent); } + /* In the vertical deck the cross axis is horizontal: flex-start would + shrink a declined card to content WIDTH, not height, so it stretches + like every other card and its height is already its own. */ + .grid > .card.declined { align-self: stretch; } } .card { position: relative; perspective: 1400px; transform: rotate(var(--fan, 0deg)); transition: transform .25s cubic-bezier(.16, 1, .3, 1); } .card:hover { transform: rotate(0deg) translateY(-4px); } @@ -660,7 +680,10 @@ function page() { and quieter, its catalog art rides as a labeled thumb in the body, and the action reads "Adopt anyway". Adoptable, never deleted: the demoted row is the hand's proof of judgment. */ - .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); } + /* Narrow AND short: without align-self the stretch default drags a thin + declined card to the tallest contender's height, a strange stilt of a + card beside the full hand. */ + .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); align-self: flex-start; } .card.declined .face { background: var(--ks-graphite); } .card.declined:hover .face { border-color: var(--ks-text-faint); } .card.declined h2 { font-size: 1rem; color: var(--ks-text); } @@ -677,6 +700,18 @@ function page() { .raises { display: flex; flex-direction: column; gap: 4px; margin: 2px 0; } .raise { font-size: .78rem; color: var(--ks-text-muted); line-height: 1.45; border-left: 2px solid var(--ks-patina); padding-left: 8px; } .raise .fact-label { color: var(--ks-patina); } + /* Several raises cycle instead of stacking: one visible at a time, a + counter for the rest, the whole block advances on click. */ + .raises-cycle { cursor: pointer; border-radius: 6px; } + .raises-cycle .raise { display: none; border-left: none; padding-left: 0; } + .raises-cycle .raise.active { display: block; } + .raises-cycle { border-left: 2px solid var(--ks-patina); padding-left: 8px; } + .raises-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; } + .raises-head .fact-label { color: var(--ks-patina); } + .raises-count { font-family: var(--ks-mono); font-size: .58rem; letter-spacing: .14em; color: var(--ks-text-faint); } + .raises-count::after { content: " \\203A"; } + .raises-cycle:hover .raises-count { color: var(--ks-patina); } + .sr-live { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } /* The standing exit as a card: present with full anatomy, never dressed as a contender. Graphite instead of kinpaku, and it never takes the lead ring. */ .card.canon .face { border-color: var(--ks-rule); background: var(--ks-graphite); } @@ -776,6 +811,25 @@ function page() { b.closest('.card').classList.toggle('flipped'); })); + // Raise cycler: click (or Enter) advances to the next donation. + document.querySelectorAll('.raises-cycle').forEach(cycle => { + const raises = [...cycle.querySelectorAll('.raise')]; + const count = cycle.querySelector('[data-raises-count]'); + let at = 0; + const live = cycle.querySelector('.sr-live'); + const show = (announce) => { + raises.forEach((raise, i) => raise.classList.toggle('active', i === at)); + if (count) count.textContent = (at + 1) + '/' + raises.length; + // Screen readers hear the raise they just advanced to; the initial + // render stays quiet so page load does not narrate every card. + if (announce && live) live.textContent = 'Raise ' + (at + 1) + ' of ' + raises.length + ': ' + (raises[at]?.textContent || ''); + }; + show(false); + const advance = (e) => { e.stopPropagation(); at = (at + 1) % raises.length; show(true); }; + cycle.addEventListener('click', advance); + cycle.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); advance(e); } }); + }); + // Deal from the stack: cards begin piled at the grid's center, blurred, // then travel to their seats with a stagger. const cards = [...document.querySelectorAll('.card')]; @@ -823,7 +877,7 @@ function page() { const note = m.querySelector('.sketch-note'); const started = Date.now(); // A live elapsed count is the difference between "working" and "frozen". - const tick = setInterval(() => { if (note) note.textContent = 'sketching · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); + const tick = setInterval(() => { if (note) note.textContent = 'rendering · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); const settle = () => { clearInterval(tick); m.classList.remove('sketching', 'stand-in'); m.querySelector('.shimmer')?.remove(); m.querySelector('.stand-in-label')?.remove(); }; const standIn = () => { const pip = m.querySelector('.pip img'); @@ -834,7 +888,7 @@ function page() { clearInterval(tick); const label = document.createElement('p'); label.className = 'stand-in-label'; - label.textContent = 'inspiration · sketch pending'; + label.textContent = 'inspiration · comp pending'; m.appendChild(label); }; const tryLoad = () => { diff --git a/.vibe/skills/impeccable/reference/degraded/asset-producer.md b/.vibe/skills/impeccable/reference/degraded/asset-producer.md index 5e4292806..71c4f3245 100644 --- a/.vibe/skills/impeccable/reference/degraded/asset-producer.md +++ b/.vibe/skills/impeccable/reference/degraded/asset-producer.md @@ -11,9 +11,9 @@ Your job is production cleanup, not new art direction. Work only from the approv Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster. -## Decision Sketches +## Decision Comps -When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame, including its aspect: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. The frame is shared across siblings so no sketch looks more finished than another; a finish gap breaks the comparison. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a sketch run. +When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `sketch` path (the field keeps its wire name) the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a comp is reported back, not padded from imagination. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (its regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment is what keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run. ## Input Contract diff --git a/.vibe/skills/impeccable/reference/degraded/finish-reviewer.md b/.vibe/skills/impeccable/reference/degraded/finish-reviewer.md index c49acadb0..c7bb7ca85 100644 --- a/.vibe/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.vibe/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,12 +11,12 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path; and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order -1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comps with no recorded pick mean the approval point was skipped, and that is a material finding. -2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. +1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comp-round comps with no recorded pick mean the approval point was skipped, and that is a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and they imply no approval whatever the build path; a code-led build has no comp round at all. +2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. A critique-reference comp, when one arrived on such a build, is provocation rather than spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is the question of what the image dared that the build did not, and the dares worth adopting enter material_fixes as ordinary ordered fixes. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. 3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition. 4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport. 5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material. diff --git a/.vibe/skills/impeccable/reference/new-work.md b/.vibe/skills/impeccable/reference/new-work.md index 4adf881d6..0120e092c 100644 --- a/.vibe/skills/impeccable/reference/new-work.md +++ b/.vibe/skills/impeccable/reference/new-work.md @@ -48,7 +48,7 @@ The script assigns which structure gets built; your top-ranked structure is what The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .vibe/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. +When image generation exists, every card also declares a `sketch` path under `.impeccable/mocks/decision/` (the field keeps its wire name for compatibility; what it carries is the card's comp), the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the comps; the page shimmer-waits per slot and the user may answer before they land. Each card's image is that direction's north-star comp at full fidelity, produced under the comp discipline in [visualize.md](visualize.md): the requested surface's first viewport, structure-led prompt, real product name and real content, no invented commercial claims, in that card's own palette, type character, and material world, committed all the way. Generation takes the same time at any fidelity, so an unfinished sketch pays sketch quality for comp cost; fairness between cards comes from equal fidelity in each card's own grammar, one surface, one aspect, never from shared unfinishedness. The frame's aspect is the surface's own: a native app or mobile-first surface comps portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen comped landscape is a broken frame, not a neutral default. Produce in the order the user reads, the assigned card, then the pick, then the full-card hand, then canon, each file written with its prompt sidecar the moment it is done, so a re-roll's spend front-loads onto the cards read first; declined challengers get no comp, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-comp packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. The chosen card's comp is not spent by the choice: on a comp-led build it enters the comp round as compositional option one, and on a code-led build it returns at the finish review as the critique reference, what the image dared that the build did not. The unchosen comps stay in `.impeccable/mocks/decision/` as the round's spent hand; they carry no approval and imply none. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. @@ -80,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. @@ -105,6 +105,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .vibe/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths, and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .vibe/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.vibe/skills/impeccable/reference/visualize.md b/.vibe/skills/impeccable/reference/visualize.md index a8bed5230..172762e9c 100644 --- a/.vibe/skills/impeccable/reference/visualize.md +++ b/.vibe/skills/impeccable/reference/visualize.md @@ -6,7 +6,7 @@ The purpose of a probe is to test composition, narrative, hierarchy, density, fo ## 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. A decision-page sketch is not a probe: it chose the direction at deliberately unfinished fidelity, so the three comps render regardless, and the chosen card's sketch seeds at most one of them. +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. - 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. - When the user shortlisted multiple concepts, spread the three across them. @@ -22,7 +22,7 @@ Show the three together: in the harness when it can display images, otherwise on Do not begin code until the user approves a direction or explicitly delegates the choice. If they delegate, choose using the task brief, PRODUCT.md, and DESIGN.md, and state the evidence. Approval refines the task concept; it does not modify DESIGN.md. -This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build with generated comps and no recorded approval as carrying a material finding. +This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build whose comp round produced comps with no recorded approval as carrying a material finding; decision comps under `.impeccable/mocks/decision/` are the direction round's hand, not comp-round output, and imply no approval on their own. After approval, record the choice where tools can find it: the approved comp's path goes in the surface brief, and the approved comp's `.json` prompt sidecar gains `"approved": true` (every comp generated through `generate-image.mjs` has one; create it if a native tool didn't). The sidecar travels with the mocks folder, so the approval survives sessions and machines that never see the brief. Then summarize the composition and the parts of the comp that must not be literalized, return to new-work.md, record the direction contract from the approved surface concept, and build. diff --git a/.vibe/skills/impeccable/scripts/serve-question.mjs b/.vibe/skills/impeccable/scripts/serve-question.mjs index bbbbdf77e..788211920 100644 --- a/.vibe/skills/impeccable/scripts/serve-question.mjs +++ b/.vibe/skills/impeccable/scripts/serve-question.mjs @@ -42,10 +42,12 @@ * // raise lines under the identity row * "risk": "one line: the honest risk", // optional * "body": "fallback prose when the structured fields are absent", - * "sketch": ".impeccable/sketches/assigned.webp", // optional; may not exist - * // yet: the page shimmer-waits and polls the - * // slot until the file lands, so serve first - * // and generate after + * "sketch": ".impeccable/mocks/decision/assigned.webp", // optional; the card's + * // full-fidelity direction comp (the field + * // keeps the sketch era's wire name). May not + * // exist yet: the page shimmer-waits and + * // polls the slot until the file lands, so + * // serve first and generate after * "hero": "https://... or /abs/path.webp", // optional inspiration image; * // rides picture-in-picture when a sketch exists * "board": "https://... or /abs/path.webp" // optional secondary image @@ -147,7 +149,7 @@ function printAnswer(raw) { console.log("CHOSEN CARD: open the chosen world's board and hero images now, before any code. When your harness only reads files, or runs sandboxed, download them INTO the workspace and open the relative path; a sandboxed viewer rejects absolute paths outside it. They set the craft bar the build must reach."); } if (a.sketch) { - console.log('CHOSEN SKETCH: the decision sketch at that path may seed one comp probe; the comp round still renders its full set, because a sketch chose the direction, not the composition.'); + console.log('CHOSEN COMP: the decision comp at that path is compositional option one. On a comp-led build the comp round adds two variations beside it; on a code-led build it returns at the finish review as the critique reference. Never regenerate it from scratch.'); } if (a.optionId === 'canon') { console.log('CANON CHOSEN: the user picked the category standard on purpose. Ask once for two or three products this should sit alongside; their craft level becomes the quality bar. Execute the canon at full commitment, conventions embraced without irony or smuggled quirk.'); @@ -173,17 +175,17 @@ if (hasFlag('schema')) { title: 'Choose the visual world', question: 'The roll assigned Fillmore Handbill. Keep it, take an alternate, or re-roll.', options: [ - { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, - { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/sketches/model-pick.webp' }, - { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, - { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, + { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/mocks/decision/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, + { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', palette: ['#1f1c18', '#efe5d0', '#a33327'], materials: ['woodcut', 'rag paper'], viewport: 'One tall sheet, the newest release as today’s ballad.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/mocks/decision/model-pick.webp' }, + { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', palette: ['#0000c0', '#ffff00', '#00c000', '#ffffff'], materials: ['block mosaic', 'phosphor glow'], viewport: 'P100 index page, releases as numbered rows.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', risk: 'Reads retro-novelty when the grid is not strict.', sketch: '.impeccable/mocks/decision/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, + { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', palette: ['#101418', '#9fb4c0'], materials: ['film grain', 'backlit glass'], case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, ], reroll: { registers: ['safer', 'bolder'] }, canon: true, - canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', viewport: 'The arrangement a visitor expects, at full craft.', sketch: '.impeccable/sketches/canon.webp' }, + 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.', sketch: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. 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. Sketch 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/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY 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 sketch slot carries the card’s full-fidelity direction comp (the field keeps its wire name for compatibility). 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); } @@ -337,16 +339,19 @@ function loadRound(json) { sketchSrc: sketchSrc(option.sketch), }); options = parsed.options.map(decorate); - // The verdict routes rendering: full cards first, declined cards demoted to - // the end of the deck in their own payload order. The reorder happens here - // so a payload that interleaves them still renders the weighing's shape. + // The verdict routes rendering: full cards first, then the canon, then the + // declined cards dead last in their own payload order. The reorder happens + // here so a payload that interleaves them still renders the weighing's + // shape, and the deck reads as a gradient of standing: contenders, the + // familiar door, then the demoted row. const declined = options.filter((o) => o.verdict === 'declined'); - if (declined.length) options = [...options.filter((o) => o.verdict !== 'declined'), ...declined]; + options = options.filter((o) => o.verdict !== 'declined'); // The standing exit as a full card: same anatomy, reserved id, rendered // subordinate by the page. Without it, canon stays the quiet footer action. if (parsed.canonCard && typeof parsed.canonCard === 'object') { options = [...options, { ...decorate(parsed.canonCard), id: 'canon', isCanon: true }]; } + options = [...options, ...declined]; } try { loadRound(raw); } catch (error) { console.error(`serve-question: ${error.message}`); process.exit(1); } const detachedKey = hasFlag('detached-serve') ? arg('key') : null; @@ -392,10 +397,21 @@ function page() { if (idBits.length) rows.push(`
${idBits.join('')}
`); // Donations from declined challengers render as named raise lines: the // assigned card arrives already raised by the hand it beat, and the raise - // is readable, because a raise nobody can read did not happen. + // is readable, because a raise nobody can read did not happen. One raise + // renders inline; several become a compact cycler (click advances), so a + // generous hand cannot blow the card out of proportion. if (Array.isArray(option.raised) && option.raised.length) { const nameOf = (id) => options.find((o) => o.id === id)?.label || String(id ?? ''); - rows.push(`
${option.raised.slice(0, 4).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`).join('')}
`); + const raiseLines = option.raised.slice(0, 6).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`); + if (raiseLines.length > 1) { + rows.push(`
+
Raised by the hand1/${raiseLines.length}
+ ${raiseLines.join('')} + +
`); + } else { + rows.push(`
${raiseLines[0]}
`); + } } // Demoted art stays reachable as a labeled thumb: the catalog world // explains where the direction comes from without buying it back the @@ -438,7 +454,7 @@ function page() { if (thumbOnly(option)) return ''; if (faceSketch(option)) { return `
-
sketching…
+
rendering…
${inspiration}
${expandChip}${details}
@@ -568,6 +584,10 @@ function page() { .nav.next { right: auto; left: 50%; top: auto; bottom: 6px; transform: translate(-50%, 0); } .fade-prev { top: 0; left: 0; right: 0; bottom: auto; width: auto; height: 72px; background: linear-gradient(180deg, var(--ks-lacquer), transparent); } .fade-next { top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 72px; background: linear-gradient(0deg, var(--ks-lacquer), transparent); } + /* In the vertical deck the cross axis is horizontal: flex-start would + shrink a declined card to content WIDTH, not height, so it stretches + like every other card and its height is already its own. */ + .grid > .card.declined { align-self: stretch; } } .card { position: relative; perspective: 1400px; transform: rotate(var(--fan, 0deg)); transition: transform .25s cubic-bezier(.16, 1, .3, 1); } .card:hover { transform: rotate(0deg) translateY(-4px); } @@ -660,7 +680,10 @@ function page() { and quieter, its catalog art rides as a labeled thumb in the body, and the action reads "Adopt anyway". Adoptable, never deleted: the demoted row is the hand's proof of judgment. */ - .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); } + /* Narrow AND short: without align-self the stretch default drags a thin + declined card to the tallest contender's height, a strange stilt of a + card beside the full hand. */ + .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); align-self: flex-start; } .card.declined .face { background: var(--ks-graphite); } .card.declined:hover .face { border-color: var(--ks-text-faint); } .card.declined h2 { font-size: 1rem; color: var(--ks-text); } @@ -677,6 +700,18 @@ function page() { .raises { display: flex; flex-direction: column; gap: 4px; margin: 2px 0; } .raise { font-size: .78rem; color: var(--ks-text-muted); line-height: 1.45; border-left: 2px solid var(--ks-patina); padding-left: 8px; } .raise .fact-label { color: var(--ks-patina); } + /* Several raises cycle instead of stacking: one visible at a time, a + counter for the rest, the whole block advances on click. */ + .raises-cycle { cursor: pointer; border-radius: 6px; } + .raises-cycle .raise { display: none; border-left: none; padding-left: 0; } + .raises-cycle .raise.active { display: block; } + .raises-cycle { border-left: 2px solid var(--ks-patina); padding-left: 8px; } + .raises-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; } + .raises-head .fact-label { color: var(--ks-patina); } + .raises-count { font-family: var(--ks-mono); font-size: .58rem; letter-spacing: .14em; color: var(--ks-text-faint); } + .raises-count::after { content: " \\203A"; } + .raises-cycle:hover .raises-count { color: var(--ks-patina); } + .sr-live { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } /* The standing exit as a card: present with full anatomy, never dressed as a contender. Graphite instead of kinpaku, and it never takes the lead ring. */ .card.canon .face { border-color: var(--ks-rule); background: var(--ks-graphite); } @@ -776,6 +811,25 @@ function page() { b.closest('.card').classList.toggle('flipped'); })); + // Raise cycler: click (or Enter) advances to the next donation. + document.querySelectorAll('.raises-cycle').forEach(cycle => { + const raises = [...cycle.querySelectorAll('.raise')]; + const count = cycle.querySelector('[data-raises-count]'); + let at = 0; + const live = cycle.querySelector('.sr-live'); + const show = (announce) => { + raises.forEach((raise, i) => raise.classList.toggle('active', i === at)); + if (count) count.textContent = (at + 1) + '/' + raises.length; + // Screen readers hear the raise they just advanced to; the initial + // render stays quiet so page load does not narrate every card. + if (announce && live) live.textContent = 'Raise ' + (at + 1) + ' of ' + raises.length + ': ' + (raises[at]?.textContent || ''); + }; + show(false); + const advance = (e) => { e.stopPropagation(); at = (at + 1) % raises.length; show(true); }; + cycle.addEventListener('click', advance); + cycle.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); advance(e); } }); + }); + // Deal from the stack: cards begin piled at the grid's center, blurred, // then travel to their seats with a stagger. const cards = [...document.querySelectorAll('.card')]; @@ -823,7 +877,7 @@ function page() { const note = m.querySelector('.sketch-note'); const started = Date.now(); // A live elapsed count is the difference between "working" and "frozen". - const tick = setInterval(() => { if (note) note.textContent = 'sketching · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); + const tick = setInterval(() => { if (note) note.textContent = 'rendering · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); const settle = () => { clearInterval(tick); m.classList.remove('sketching', 'stand-in'); m.querySelector('.shimmer')?.remove(); m.querySelector('.stand-in-label')?.remove(); }; const standIn = () => { const pip = m.querySelector('.pip img'); @@ -834,7 +888,7 @@ function page() { clearInterval(tick); const label = document.createElement('p'); label.className = 'stand-in-label'; - label.textContent = 'inspiration · sketch pending'; + label.textContent = 'inspiration · comp pending'; m.appendChild(label); }; const tryLoad = () => { diff --git a/plugin/agents/impeccable-asset-producer.md b/plugin/agents/impeccable-asset-producer.md index 2ee6a9439..600f0f8f4 100644 --- a/plugin/agents/impeccable-asset-producer.md +++ b/plugin/agents/impeccable-asset-producer.md @@ -16,9 +16,9 @@ Your job is production cleanup, not new art direction. Work only from the approv Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster. -## Decision Sketches +## Decision Comps -When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame, including its aspect: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. The frame is shared across siblings so no sketch looks more finished than another; a finish gap breaks the comparison. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a sketch run. +When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `sketch` path (the field keeps its wire name) the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a comp is reported back, not padded from imagination. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (its regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment is what keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run. ## Input Contract diff --git a/plugin/agents/impeccable-finish-reviewer.md b/plugin/agents/impeccable-finish-reviewer.md index 7c71679c1..02658bf8e 100644 --- a/plugin/agents/impeccable-finish-reviewer.md +++ b/plugin/agents/impeccable-finish-reviewer.md @@ -16,12 +16,12 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path; and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order -1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comps with no recorded pick mean the approval point was skipped, and that is a material finding. -2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. +1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comp-round comps with no recorded pick mean the approval point was skipped, and that is a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and they imply no approval whatever the build path; a code-led build has no comp round at all. +2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. A critique-reference comp, when one arrived on such a build, is provocation rather than spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is the question of what the image dared that the build did not, and the dares worth adopting enter material_fixes as ordinary ordered fixes. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. 3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition. 4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport. 5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material. diff --git a/plugin/skills/impeccable/reference/degraded/asset-producer.md b/plugin/skills/impeccable/reference/degraded/asset-producer.md index 34829b4ad..cfce6a1f0 100644 --- a/plugin/skills/impeccable/reference/degraded/asset-producer.md +++ b/plugin/skills/impeccable/reference/degraded/asset-producer.md @@ -11,9 +11,9 @@ Your job is production cleanup, not new art direction. Work only from the approv Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster. -## Decision Sketches +## Decision Comps -When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame, including its aspect: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. The frame is shared across siblings so no sketch looks more finished than another; a finish gap breaks the comparison. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a sketch run. +When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `sketch` path (the field keeps its wire name) the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a comp is reported back, not padded from imagination. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (its regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment is what keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run. ## Input Contract diff --git a/plugin/skills/impeccable/reference/degraded/finish-reviewer.md b/plugin/skills/impeccable/reference/degraded/finish-reviewer.md index c49acadb0..c7bb7ca85 100644 --- a/plugin/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/plugin/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,12 +11,12 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path; and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order -1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comps with no recorded pick mean the approval point was skipped, and that is a material finding. -2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. +1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comp-round comps with no recorded pick mean the approval point was skipped, and that is a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and they imply no approval whatever the build path; a code-led build has no comp round at all. +2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. A critique-reference comp, when one arrived on such a build, is provocation rather than spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is the question of what the image dared that the build did not, and the dares worth adopting enter material_fixes as ordinary ordered fixes. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. 3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition. 4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport. 5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material. diff --git a/plugin/skills/impeccable/reference/new-work.md b/plugin/skills/impeccable/reference/new-work.md index 718b4bd0b..961d73bb3 100644 --- a/plugin/skills/impeccable/reference/new-work.md +++ b/plugin/skills/impeccable/reference/new-work.md @@ -48,7 +48,7 @@ The script assigns which structure gets built; your top-ranked structure is what The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .claude/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. +When image generation exists, every card also declares a `sketch` path under `.impeccable/mocks/decision/` (the field keeps its wire name for compatibility; what it carries is the card's comp), the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the comps; the page shimmer-waits per slot and the user may answer before they land. Each card's image is that direction's north-star comp at full fidelity, produced under the comp discipline in [visualize.md](visualize.md): the requested surface's first viewport, structure-led prompt, real product name and real content, no invented commercial claims, in that card's own palette, type character, and material world, committed all the way. Generation takes the same time at any fidelity, so an unfinished sketch pays sketch quality for comp cost; fairness between cards comes from equal fidelity in each card's own grammar, one surface, one aspect, never from shared unfinishedness. The frame's aspect is the surface's own: a native app or mobile-first surface comps portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen comped landscape is a broken frame, not a neutral default. Produce in the order the user reads, the assigned card, then the pick, then the full-card hand, then canon, each file written with its prompt sidecar the moment it is done, so a re-roll's spend front-loads onto the cards read first; declined challengers get no comp, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-comp packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. The chosen card's comp is not spent by the choice: on a comp-led build it enters the comp round as compositional option one, and on a code-led build it returns at the finish review as the critique reference, what the image dared that the build did not. The unchosen comps stay in `.impeccable/mocks/decision/` as the round's spent hand; they carry no approval and imply none. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. @@ -82,7 +82,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. @@ -107,6 +107,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .claude/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths, and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .claude/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/plugin/skills/impeccable/reference/visualize.md b/plugin/skills/impeccable/reference/visualize.md index 6df08b30b..4d91330e4 100644 --- a/plugin/skills/impeccable/reference/visualize.md +++ b/plugin/skills/impeccable/reference/visualize.md @@ -6,7 +6,7 @@ The purpose of a probe is to test composition, narrative, hierarchy, density, fo ## 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. A decision-page sketch is not a probe: it chose the direction at deliberately unfinished fidelity, so the three comps render regardless, and the chosen card's sketch seeds at most one of them. +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. - 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. - When the user shortlisted multiple concepts, spread the three across them. @@ -22,7 +22,7 @@ Show the three together: in the harness when it can display images, otherwise on Do not begin code until the user approves a direction or explicitly delegates the choice. If they delegate, choose using the task brief, PRODUCT.md, and DESIGN.md, and state the evidence. Approval refines the task concept; it does not modify DESIGN.md. -This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build with generated comps and no recorded approval as carrying a material finding. +This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build whose comp round produced comps with no recorded approval as carrying a material finding; decision comps under `.impeccable/mocks/decision/` are the direction round's hand, not comp-round output, and imply no approval on their own. After approval, record the choice where tools can find it: the approved comp's path goes in the surface brief, and the approved comp's `.json` prompt sidecar gains `"approved": true` (every comp generated through `generate-image.mjs` has one; create it if a native tool didn't). The sidecar travels with the mocks folder, so the approval survives sessions and machines that never see the brief. Then summarize the composition and the parts of the comp that must not be literalized, return to new-work.md, record the direction contract from the approved surface concept, and build. diff --git a/plugin/skills/impeccable/scripts/serve-question.mjs b/plugin/skills/impeccable/scripts/serve-question.mjs index bbbbdf77e..788211920 100644 --- a/plugin/skills/impeccable/scripts/serve-question.mjs +++ b/plugin/skills/impeccable/scripts/serve-question.mjs @@ -42,10 +42,12 @@ * // raise lines under the identity row * "risk": "one line: the honest risk", // optional * "body": "fallback prose when the structured fields are absent", - * "sketch": ".impeccable/sketches/assigned.webp", // optional; may not exist - * // yet: the page shimmer-waits and polls the - * // slot until the file lands, so serve first - * // and generate after + * "sketch": ".impeccable/mocks/decision/assigned.webp", // optional; the card's + * // full-fidelity direction comp (the field + * // keeps the sketch era's wire name). May not + * // exist yet: the page shimmer-waits and + * // polls the slot until the file lands, so + * // serve first and generate after * "hero": "https://... or /abs/path.webp", // optional inspiration image; * // rides picture-in-picture when a sketch exists * "board": "https://... or /abs/path.webp" // optional secondary image @@ -147,7 +149,7 @@ function printAnswer(raw) { console.log("CHOSEN CARD: open the chosen world's board and hero images now, before any code. When your harness only reads files, or runs sandboxed, download them INTO the workspace and open the relative path; a sandboxed viewer rejects absolute paths outside it. They set the craft bar the build must reach."); } if (a.sketch) { - console.log('CHOSEN SKETCH: the decision sketch at that path may seed one comp probe; the comp round still renders its full set, because a sketch chose the direction, not the composition.'); + console.log('CHOSEN COMP: the decision comp at that path is compositional option one. On a comp-led build the comp round adds two variations beside it; on a code-led build it returns at the finish review as the critique reference. Never regenerate it from scratch.'); } if (a.optionId === 'canon') { console.log('CANON CHOSEN: the user picked the category standard on purpose. Ask once for two or three products this should sit alongside; their craft level becomes the quality bar. Execute the canon at full commitment, conventions embraced without irony or smuggled quirk.'); @@ -173,17 +175,17 @@ if (hasFlag('schema')) { title: 'Choose the visual world', question: 'The roll assigned Fillmore Handbill. Keep it, take an alternate, or re-roll.', options: [ - { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, - { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/sketches/model-pick.webp' }, - { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, - { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, + { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/mocks/decision/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, + { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', palette: ['#1f1c18', '#efe5d0', '#a33327'], materials: ['woodcut', 'rag paper'], viewport: 'One tall sheet, the newest release as today’s ballad.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/mocks/decision/model-pick.webp' }, + { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', palette: ['#0000c0', '#ffff00', '#00c000', '#ffffff'], materials: ['block mosaic', 'phosphor glow'], viewport: 'P100 index page, releases as numbered rows.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', risk: 'Reads retro-novelty when the grid is not strict.', sketch: '.impeccable/mocks/decision/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, + { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', palette: ['#101418', '#9fb4c0'], materials: ['film grain', 'backlit glass'], case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, ], reroll: { registers: ['safer', 'bolder'] }, canon: true, - canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', viewport: 'The arrangement a visitor expects, at full craft.', sketch: '.impeccable/sketches/canon.webp' }, + 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.', sketch: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. 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. Sketch 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/sketch accept URLs or local paths; sketch 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 sketch, 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 sketch (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 "MY 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 sketch slot carries the card’s full-fidelity direction comp (the field keeps its wire name for compatibility). 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); } @@ -337,16 +339,19 @@ function loadRound(json) { sketchSrc: sketchSrc(option.sketch), }); options = parsed.options.map(decorate); - // The verdict routes rendering: full cards first, declined cards demoted to - // the end of the deck in their own payload order. The reorder happens here - // so a payload that interleaves them still renders the weighing's shape. + // The verdict routes rendering: full cards first, then the canon, then the + // declined cards dead last in their own payload order. The reorder happens + // here so a payload that interleaves them still renders the weighing's + // shape, and the deck reads as a gradient of standing: contenders, the + // familiar door, then the demoted row. const declined = options.filter((o) => o.verdict === 'declined'); - if (declined.length) options = [...options.filter((o) => o.verdict !== 'declined'), ...declined]; + options = options.filter((o) => o.verdict !== 'declined'); // The standing exit as a full card: same anatomy, reserved id, rendered // subordinate by the page. Without it, canon stays the quiet footer action. if (parsed.canonCard && typeof parsed.canonCard === 'object') { options = [...options, { ...decorate(parsed.canonCard), id: 'canon', isCanon: true }]; } + options = [...options, ...declined]; } try { loadRound(raw); } catch (error) { console.error(`serve-question: ${error.message}`); process.exit(1); } const detachedKey = hasFlag('detached-serve') ? arg('key') : null; @@ -392,10 +397,21 @@ function page() { if (idBits.length) rows.push(`
${idBits.join('')}
`); // Donations from declined challengers render as named raise lines: the // assigned card arrives already raised by the hand it beat, and the raise - // is readable, because a raise nobody can read did not happen. + // is readable, because a raise nobody can read did not happen. One raise + // renders inline; several become a compact cycler (click advances), so a + // generous hand cannot blow the card out of proportion. if (Array.isArray(option.raised) && option.raised.length) { const nameOf = (id) => options.find((o) => o.id === id)?.label || String(id ?? ''); - rows.push(`
${option.raised.slice(0, 4).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`).join('')}
`); + const raiseLines = option.raised.slice(0, 6).map((r) => `

Raised by ${esc(nameOf(r.from))}${esc(r.raise || r.kept || '')}

`); + if (raiseLines.length > 1) { + rows.push(`
+
Raised by the hand1/${raiseLines.length}
+ ${raiseLines.join('')} + +
`); + } else { + rows.push(`
${raiseLines[0]}
`); + } } // Demoted art stays reachable as a labeled thumb: the catalog world // explains where the direction comes from without buying it back the @@ -438,7 +454,7 @@ function page() { if (thumbOnly(option)) return ''; if (faceSketch(option)) { return `
-
sketching…
+
rendering…
${inspiration}
${expandChip}${details}
@@ -568,6 +584,10 @@ function page() { .nav.next { right: auto; left: 50%; top: auto; bottom: 6px; transform: translate(-50%, 0); } .fade-prev { top: 0; left: 0; right: 0; bottom: auto; width: auto; height: 72px; background: linear-gradient(180deg, var(--ks-lacquer), transparent); } .fade-next { top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 72px; background: linear-gradient(0deg, var(--ks-lacquer), transparent); } + /* In the vertical deck the cross axis is horizontal: flex-start would + shrink a declined card to content WIDTH, not height, so it stretches + like every other card and its height is already its own. */ + .grid > .card.declined { align-self: stretch; } } .card { position: relative; perspective: 1400px; transform: rotate(var(--fan, 0deg)); transition: transform .25s cubic-bezier(.16, 1, .3, 1); } .card:hover { transform: rotate(0deg) translateY(-4px); } @@ -660,7 +680,10 @@ function page() { and quieter, its catalog art rides as a labeled thumb in the body, and the action reads "Adopt anyway". Adoptable, never deleted: the demoted row is the hand's proof of judgment. */ - .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); } + /* Narrow AND short: without align-self the stretch default drags a thin + declined card to the tallest contender's height, a strange stilt of a + card beside the full hand. */ + .grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); align-self: flex-start; } .card.declined .face { background: var(--ks-graphite); } .card.declined:hover .face { border-color: var(--ks-text-faint); } .card.declined h2 { font-size: 1rem; color: var(--ks-text); } @@ -677,6 +700,18 @@ function page() { .raises { display: flex; flex-direction: column; gap: 4px; margin: 2px 0; } .raise { font-size: .78rem; color: var(--ks-text-muted); line-height: 1.45; border-left: 2px solid var(--ks-patina); padding-left: 8px; } .raise .fact-label { color: var(--ks-patina); } + /* Several raises cycle instead of stacking: one visible at a time, a + counter for the rest, the whole block advances on click. */ + .raises-cycle { cursor: pointer; border-radius: 6px; } + .raises-cycle .raise { display: none; border-left: none; padding-left: 0; } + .raises-cycle .raise.active { display: block; } + .raises-cycle { border-left: 2px solid var(--ks-patina); padding-left: 8px; } + .raises-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; } + .raises-head .fact-label { color: var(--ks-patina); } + .raises-count { font-family: var(--ks-mono); font-size: .58rem; letter-spacing: .14em; color: var(--ks-text-faint); } + .raises-count::after { content: " \\203A"; } + .raises-cycle:hover .raises-count { color: var(--ks-patina); } + .sr-live { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } /* The standing exit as a card: present with full anatomy, never dressed as a contender. Graphite instead of kinpaku, and it never takes the lead ring. */ .card.canon .face { border-color: var(--ks-rule); background: var(--ks-graphite); } @@ -776,6 +811,25 @@ function page() { b.closest('.card').classList.toggle('flipped'); })); + // Raise cycler: click (or Enter) advances to the next donation. + document.querySelectorAll('.raises-cycle').forEach(cycle => { + const raises = [...cycle.querySelectorAll('.raise')]; + const count = cycle.querySelector('[data-raises-count]'); + let at = 0; + const live = cycle.querySelector('.sr-live'); + const show = (announce) => { + raises.forEach((raise, i) => raise.classList.toggle('active', i === at)); + if (count) count.textContent = (at + 1) + '/' + raises.length; + // Screen readers hear the raise they just advanced to; the initial + // render stays quiet so page load does not narrate every card. + if (announce && live) live.textContent = 'Raise ' + (at + 1) + ' of ' + raises.length + ': ' + (raises[at]?.textContent || ''); + }; + show(false); + const advance = (e) => { e.stopPropagation(); at = (at + 1) % raises.length; show(true); }; + cycle.addEventListener('click', advance); + cycle.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); advance(e); } }); + }); + // Deal from the stack: cards begin piled at the grid's center, blurred, // then travel to their seats with a stagger. const cards = [...document.querySelectorAll('.card')]; @@ -823,7 +877,7 @@ function page() { const note = m.querySelector('.sketch-note'); const started = Date.now(); // A live elapsed count is the difference between "working" and "frozen". - const tick = setInterval(() => { if (note) note.textContent = 'sketching · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); + const tick = setInterval(() => { if (note) note.textContent = 'rendering · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000); const settle = () => { clearInterval(tick); m.classList.remove('sketching', 'stand-in'); m.querySelector('.shimmer')?.remove(); m.querySelector('.stand-in-label')?.remove(); }; const standIn = () => { const pip = m.querySelector('.pip img'); @@ -834,7 +888,7 @@ function page() { clearInterval(tick); const label = document.createElement('p'); label.className = 'stand-in-label'; - label.textContent = 'inspiration · sketch pending'; + label.textContent = 'inspiration · comp pending'; m.appendChild(label); }; const tryLoad = () => { From fc05472a20d6e3de316d118a9fb7244fe151c7f7 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Sat, 8 Aug 2026 15:49:43 -0700 Subject: [PATCH 05/29] Restore reduced-motion animation guidance (#540) * Restore reduced-motion build guidance Restores the accessibility requirement and verification step to the animation playbook, with a regression test that keeps it on the build path. Implemented and validated with OpenAI Codex assistance under standing maintainer authorization. * Harden reduced-motion guidance regression Normalizes CRLF input and accepts either reduced-motion spelling so the contract stays portable and intent-focused. Implemented and validated with OpenAI Codex assistance under standing maintainer authorization. * Anchor skill reference test to its module Resolve the repository fixture path from the test module so the regression test is independent of the caller's working directory. This change was prepared with AI assistance under maintainer authorization. * Clarify reduced-motion guidance Replace the double negative in the canonical animation guidance and keep the source contract aligned with the clearer wording. This change was prepared with AI assistance under maintainer authorization. --- scripts/test-suites.mjs | 1 + skill/reference/animate.md | 3 +++ tests/skill-reference.test.mjs | 20 ++++++++++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 tests/skill-reference.test.mjs diff --git a/scripts/test-suites.mjs b/scripts/test-suites.mjs index 4145cfc83..608cea352 100644 --- a/scripts/test-suites.mjs +++ b/scripts/test-suites.mjs @@ -68,6 +68,7 @@ export const SUITES = { 'tests/release.test.mjs', 'tests/doctor.test.mjs', 'tests/staleness.test.mjs', + 'tests/skill-reference.test.mjs', 'tests/target-args.test.mjs', 'tests/surface-brief.test.mjs', 'tests/template-extensions.test.mjs', diff --git a/skill/reference/animate.md b/skill/reference/animate.md index eea0b4781..f80ebd154 100644 --- a/skill/reference/animate.md +++ b/skill/reference/animate.md @@ -74,12 +74,15 @@ Keep content visible in the default state so failed scripts do not hide the page Respect autoplay and sound preferences. Any nonessential loop must stop when offscreen or hidden. +Every web animation needs a `prefers-reduced-motion` path with an intentional alternative. Remove or reduce spatial movement while preserving opacity, color, and state transitions that carry meaning. Reduced motion means fewer and gentler animations, not disabling all motion; feedback that confirms an action should remain legible. + ## Verify - The focal motion is specific to the selected world and surface. - Every supporting animation explains feedback, state, or relationship. - Interruption and repeated use behave correctly. - Desktop, mobile, and keyboard paths remain usable. +- The `prefers-reduced-motion` path reduces movement without erasing meaningful feedback or state changes. - Expensive effects stay smooth on the target device. - Removing an animation would lose meaning or authored character, not merely decoration. diff --git a/tests/skill-reference.test.mjs b/tests/skill-reference.test.mjs new file mode 100644 index 000000000..ea1517591 --- /dev/null +++ b/tests/skill-reference.test.mjs @@ -0,0 +1,20 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = fileURLToPath(new URL('..', import.meta.url)); + +describe('skill reference authoring contracts', () => { + it('keeps reduced-motion guidance on the animation build path', () => { + const animate = readFileSync(join(ROOT, 'skill/reference/animate.md'), 'utf-8').replace(/\r\n?/g, '\n'); + const accessibility = animate.match(/## Accessibility and control\n([\s\S]*?)\n## Verify/)?.[1] ?? ''; + const verify = animate.match(/## Verify\n([\s\S]*?)(?:\n## |$)/)?.[1] ?? ''; + + assert.match(accessibility, /prefers-reduced-motion/); + assert.match(accessibility, /intentional alternative/); + assert.match(accessibility, /not disabling all motion/); + assert.match(verify, /reduced[- ]motion/i); + }); +}); From 5d10bc842cbccd2ae7d3a88296d87d3be0b125b3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 22:50:13 +0000 Subject: [PATCH 06/29] Sync generated provider output --- .agents/skills/impeccable/reference/animate.md | 3 +++ .claude/skills/impeccable/reference/animate.md | 3 +++ .cursor/skills/impeccable/reference/animate.md | 3 +++ .gemini/skills/impeccable/reference/animate.md | 3 +++ .github/skills/impeccable/reference/animate.md | 3 +++ .grok/skills/impeccable/reference/animate.md | 3 +++ .kiro/skills/impeccable/reference/animate.md | 3 +++ .opencode/skills/impeccable/reference/animate.md | 3 +++ .pi/skills/impeccable/reference/animate.md | 3 +++ .qoder/skills/impeccable/reference/animate.md | 3 +++ .rovodev/skills/impeccable/reference/animate.md | 3 +++ .trae-cn/skills/impeccable/reference/animate.md | 3 +++ .trae/skills/impeccable/reference/animate.md | 3 +++ .vibe/skills/impeccable/reference/animate.md | 3 +++ plugin/skills/impeccable/reference/animate.md | 3 +++ 15 files changed, 45 insertions(+) diff --git a/.agents/skills/impeccable/reference/animate.md b/.agents/skills/impeccable/reference/animate.md index 6d74d1841..c8fb9e038 100644 --- a/.agents/skills/impeccable/reference/animate.md +++ b/.agents/skills/impeccable/reference/animate.md @@ -74,12 +74,15 @@ Keep content visible in the default state so failed scripts do not hide the page Respect autoplay and sound preferences. Any nonessential loop must stop when offscreen or hidden. +Every web animation needs a `prefers-reduced-motion` path with an intentional alternative. Remove or reduce spatial movement while preserving opacity, color, and state transitions that carry meaning. Reduced motion means fewer and gentler animations, not disabling all motion; feedback that confirms an action should remain legible. + ## Verify - The focal motion is specific to the selected world and surface. - Every supporting animation explains feedback, state, or relationship. - Interruption and repeated use behave correctly. - Desktop, mobile, and keyboard paths remain usable. +- The `prefers-reduced-motion` path reduces movement without erasing meaningful feedback or state changes. - Expensive effects stay smooth on the target device. - Removing an animation would lose meaning or authored character, not merely decoration. diff --git a/.claude/skills/impeccable/reference/animate.md b/.claude/skills/impeccable/reference/animate.md index d2e340763..4ae4cc5fc 100644 --- a/.claude/skills/impeccable/reference/animate.md +++ b/.claude/skills/impeccable/reference/animate.md @@ -74,12 +74,15 @@ Keep content visible in the default state so failed scripts do not hide the page Respect autoplay and sound preferences. Any nonessential loop must stop when offscreen or hidden. +Every web animation needs a `prefers-reduced-motion` path with an intentional alternative. Remove or reduce spatial movement while preserving opacity, color, and state transitions that carry meaning. Reduced motion means fewer and gentler animations, not disabling all motion; feedback that confirms an action should remain legible. + ## Verify - The focal motion is specific to the selected world and surface. - Every supporting animation explains feedback, state, or relationship. - Interruption and repeated use behave correctly. - Desktop, mobile, and keyboard paths remain usable. +- The `prefers-reduced-motion` path reduces movement without erasing meaningful feedback or state changes. - Expensive effects stay smooth on the target device. - Removing an animation would lose meaning or authored character, not merely decoration. diff --git a/.cursor/skills/impeccable/reference/animate.md b/.cursor/skills/impeccable/reference/animate.md index d2e340763..4ae4cc5fc 100644 --- a/.cursor/skills/impeccable/reference/animate.md +++ b/.cursor/skills/impeccable/reference/animate.md @@ -74,12 +74,15 @@ Keep content visible in the default state so failed scripts do not hide the page Respect autoplay and sound preferences. Any nonessential loop must stop when offscreen or hidden. +Every web animation needs a `prefers-reduced-motion` path with an intentional alternative. Remove or reduce spatial movement while preserving opacity, color, and state transitions that carry meaning. Reduced motion means fewer and gentler animations, not disabling all motion; feedback that confirms an action should remain legible. + ## Verify - The focal motion is specific to the selected world and surface. - Every supporting animation explains feedback, state, or relationship. - Interruption and repeated use behave correctly. - Desktop, mobile, and keyboard paths remain usable. +- The `prefers-reduced-motion` path reduces movement without erasing meaningful feedback or state changes. - Expensive effects stay smooth on the target device. - Removing an animation would lose meaning or authored character, not merely decoration. diff --git a/.gemini/skills/impeccable/reference/animate.md b/.gemini/skills/impeccable/reference/animate.md index d2e340763..4ae4cc5fc 100644 --- a/.gemini/skills/impeccable/reference/animate.md +++ b/.gemini/skills/impeccable/reference/animate.md @@ -74,12 +74,15 @@ Keep content visible in the default state so failed scripts do not hide the page Respect autoplay and sound preferences. Any nonessential loop must stop when offscreen or hidden. +Every web animation needs a `prefers-reduced-motion` path with an intentional alternative. Remove or reduce spatial movement while preserving opacity, color, and state transitions that carry meaning. Reduced motion means fewer and gentler animations, not disabling all motion; feedback that confirms an action should remain legible. + ## Verify - The focal motion is specific to the selected world and surface. - Every supporting animation explains feedback, state, or relationship. - Interruption and repeated use behave correctly. - Desktop, mobile, and keyboard paths remain usable. +- The `prefers-reduced-motion` path reduces movement without erasing meaningful feedback or state changes. - Expensive effects stay smooth on the target device. - Removing an animation would lose meaning or authored character, not merely decoration. diff --git a/.github/skills/impeccable/reference/animate.md b/.github/skills/impeccable/reference/animate.md index d2e340763..4ae4cc5fc 100644 --- a/.github/skills/impeccable/reference/animate.md +++ b/.github/skills/impeccable/reference/animate.md @@ -74,12 +74,15 @@ Keep content visible in the default state so failed scripts do not hide the page Respect autoplay and sound preferences. Any nonessential loop must stop when offscreen or hidden. +Every web animation needs a `prefers-reduced-motion` path with an intentional alternative. Remove or reduce spatial movement while preserving opacity, color, and state transitions that carry meaning. Reduced motion means fewer and gentler animations, not disabling all motion; feedback that confirms an action should remain legible. + ## Verify - The focal motion is specific to the selected world and surface. - Every supporting animation explains feedback, state, or relationship. - Interruption and repeated use behave correctly. - Desktop, mobile, and keyboard paths remain usable. +- The `prefers-reduced-motion` path reduces movement without erasing meaningful feedback or state changes. - Expensive effects stay smooth on the target device. - Removing an animation would lose meaning or authored character, not merely decoration. diff --git a/.grok/skills/impeccable/reference/animate.md b/.grok/skills/impeccable/reference/animate.md index d2e340763..4ae4cc5fc 100644 --- a/.grok/skills/impeccable/reference/animate.md +++ b/.grok/skills/impeccable/reference/animate.md @@ -74,12 +74,15 @@ Keep content visible in the default state so failed scripts do not hide the page Respect autoplay and sound preferences. Any nonessential loop must stop when offscreen or hidden. +Every web animation needs a `prefers-reduced-motion` path with an intentional alternative. Remove or reduce spatial movement while preserving opacity, color, and state transitions that carry meaning. Reduced motion means fewer and gentler animations, not disabling all motion; feedback that confirms an action should remain legible. + ## Verify - The focal motion is specific to the selected world and surface. - Every supporting animation explains feedback, state, or relationship. - Interruption and repeated use behave correctly. - Desktop, mobile, and keyboard paths remain usable. +- The `prefers-reduced-motion` path reduces movement without erasing meaningful feedback or state changes. - Expensive effects stay smooth on the target device. - Removing an animation would lose meaning or authored character, not merely decoration. diff --git a/.kiro/skills/impeccable/reference/animate.md b/.kiro/skills/impeccable/reference/animate.md index d2e340763..4ae4cc5fc 100644 --- a/.kiro/skills/impeccable/reference/animate.md +++ b/.kiro/skills/impeccable/reference/animate.md @@ -74,12 +74,15 @@ Keep content visible in the default state so failed scripts do not hide the page Respect autoplay and sound preferences. Any nonessential loop must stop when offscreen or hidden. +Every web animation needs a `prefers-reduced-motion` path with an intentional alternative. Remove or reduce spatial movement while preserving opacity, color, and state transitions that carry meaning. Reduced motion means fewer and gentler animations, not disabling all motion; feedback that confirms an action should remain legible. + ## Verify - The focal motion is specific to the selected world and surface. - Every supporting animation explains feedback, state, or relationship. - Interruption and repeated use behave correctly. - Desktop, mobile, and keyboard paths remain usable. +- The `prefers-reduced-motion` path reduces movement without erasing meaningful feedback or state changes. - Expensive effects stay smooth on the target device. - Removing an animation would lose meaning or authored character, not merely decoration. diff --git a/.opencode/skills/impeccable/reference/animate.md b/.opencode/skills/impeccable/reference/animate.md index d2e340763..4ae4cc5fc 100644 --- a/.opencode/skills/impeccable/reference/animate.md +++ b/.opencode/skills/impeccable/reference/animate.md @@ -74,12 +74,15 @@ Keep content visible in the default state so failed scripts do not hide the page Respect autoplay and sound preferences. Any nonessential loop must stop when offscreen or hidden. +Every web animation needs a `prefers-reduced-motion` path with an intentional alternative. Remove or reduce spatial movement while preserving opacity, color, and state transitions that carry meaning. Reduced motion means fewer and gentler animations, not disabling all motion; feedback that confirms an action should remain legible. + ## Verify - The focal motion is specific to the selected world and surface. - Every supporting animation explains feedback, state, or relationship. - Interruption and repeated use behave correctly. - Desktop, mobile, and keyboard paths remain usable. +- The `prefers-reduced-motion` path reduces movement without erasing meaningful feedback or state changes. - Expensive effects stay smooth on the target device. - Removing an animation would lose meaning or authored character, not merely decoration. diff --git a/.pi/skills/impeccable/reference/animate.md b/.pi/skills/impeccable/reference/animate.md index d2e340763..4ae4cc5fc 100644 --- a/.pi/skills/impeccable/reference/animate.md +++ b/.pi/skills/impeccable/reference/animate.md @@ -74,12 +74,15 @@ Keep content visible in the default state so failed scripts do not hide the page Respect autoplay and sound preferences. Any nonessential loop must stop when offscreen or hidden. +Every web animation needs a `prefers-reduced-motion` path with an intentional alternative. Remove or reduce spatial movement while preserving opacity, color, and state transitions that carry meaning. Reduced motion means fewer and gentler animations, not disabling all motion; feedback that confirms an action should remain legible. + ## Verify - The focal motion is specific to the selected world and surface. - Every supporting animation explains feedback, state, or relationship. - Interruption and repeated use behave correctly. - Desktop, mobile, and keyboard paths remain usable. +- The `prefers-reduced-motion` path reduces movement without erasing meaningful feedback or state changes. - Expensive effects stay smooth on the target device. - Removing an animation would lose meaning or authored character, not merely decoration. diff --git a/.qoder/skills/impeccable/reference/animate.md b/.qoder/skills/impeccable/reference/animate.md index d2e340763..4ae4cc5fc 100644 --- a/.qoder/skills/impeccable/reference/animate.md +++ b/.qoder/skills/impeccable/reference/animate.md @@ -74,12 +74,15 @@ Keep content visible in the default state so failed scripts do not hide the page Respect autoplay and sound preferences. Any nonessential loop must stop when offscreen or hidden. +Every web animation needs a `prefers-reduced-motion` path with an intentional alternative. Remove or reduce spatial movement while preserving opacity, color, and state transitions that carry meaning. Reduced motion means fewer and gentler animations, not disabling all motion; feedback that confirms an action should remain legible. + ## Verify - The focal motion is specific to the selected world and surface. - Every supporting animation explains feedback, state, or relationship. - Interruption and repeated use behave correctly. - Desktop, mobile, and keyboard paths remain usable. +- The `prefers-reduced-motion` path reduces movement without erasing meaningful feedback or state changes. - Expensive effects stay smooth on the target device. - Removing an animation would lose meaning or authored character, not merely decoration. diff --git a/.rovodev/skills/impeccable/reference/animate.md b/.rovodev/skills/impeccable/reference/animate.md index d2e340763..4ae4cc5fc 100644 --- a/.rovodev/skills/impeccable/reference/animate.md +++ b/.rovodev/skills/impeccable/reference/animate.md @@ -74,12 +74,15 @@ Keep content visible in the default state so failed scripts do not hide the page Respect autoplay and sound preferences. Any nonessential loop must stop when offscreen or hidden. +Every web animation needs a `prefers-reduced-motion` path with an intentional alternative. Remove or reduce spatial movement while preserving opacity, color, and state transitions that carry meaning. Reduced motion means fewer and gentler animations, not disabling all motion; feedback that confirms an action should remain legible. + ## Verify - The focal motion is specific to the selected world and surface. - Every supporting animation explains feedback, state, or relationship. - Interruption and repeated use behave correctly. - Desktop, mobile, and keyboard paths remain usable. +- The `prefers-reduced-motion` path reduces movement without erasing meaningful feedback or state changes. - Expensive effects stay smooth on the target device. - Removing an animation would lose meaning or authored character, not merely decoration. diff --git a/.trae-cn/skills/impeccable/reference/animate.md b/.trae-cn/skills/impeccable/reference/animate.md index d2e340763..4ae4cc5fc 100644 --- a/.trae-cn/skills/impeccable/reference/animate.md +++ b/.trae-cn/skills/impeccable/reference/animate.md @@ -74,12 +74,15 @@ Keep content visible in the default state so failed scripts do not hide the page Respect autoplay and sound preferences. Any nonessential loop must stop when offscreen or hidden. +Every web animation needs a `prefers-reduced-motion` path with an intentional alternative. Remove or reduce spatial movement while preserving opacity, color, and state transitions that carry meaning. Reduced motion means fewer and gentler animations, not disabling all motion; feedback that confirms an action should remain legible. + ## Verify - The focal motion is specific to the selected world and surface. - Every supporting animation explains feedback, state, or relationship. - Interruption and repeated use behave correctly. - Desktop, mobile, and keyboard paths remain usable. +- The `prefers-reduced-motion` path reduces movement without erasing meaningful feedback or state changes. - Expensive effects stay smooth on the target device. - Removing an animation would lose meaning or authored character, not merely decoration. diff --git a/.trae/skills/impeccable/reference/animate.md b/.trae/skills/impeccable/reference/animate.md index d2e340763..4ae4cc5fc 100644 --- a/.trae/skills/impeccable/reference/animate.md +++ b/.trae/skills/impeccable/reference/animate.md @@ -74,12 +74,15 @@ Keep content visible in the default state so failed scripts do not hide the page Respect autoplay and sound preferences. Any nonessential loop must stop when offscreen or hidden. +Every web animation needs a `prefers-reduced-motion` path with an intentional alternative. Remove or reduce spatial movement while preserving opacity, color, and state transitions that carry meaning. Reduced motion means fewer and gentler animations, not disabling all motion; feedback that confirms an action should remain legible. + ## Verify - The focal motion is specific to the selected world and surface. - Every supporting animation explains feedback, state, or relationship. - Interruption and repeated use behave correctly. - Desktop, mobile, and keyboard paths remain usable. +- The `prefers-reduced-motion` path reduces movement without erasing meaningful feedback or state changes. - Expensive effects stay smooth on the target device. - Removing an animation would lose meaning or authored character, not merely decoration. diff --git a/.vibe/skills/impeccable/reference/animate.md b/.vibe/skills/impeccable/reference/animate.md index d2e340763..4ae4cc5fc 100644 --- a/.vibe/skills/impeccable/reference/animate.md +++ b/.vibe/skills/impeccable/reference/animate.md @@ -74,12 +74,15 @@ Keep content visible in the default state so failed scripts do not hide the page Respect autoplay and sound preferences. Any nonessential loop must stop when offscreen or hidden. +Every web animation needs a `prefers-reduced-motion` path with an intentional alternative. Remove or reduce spatial movement while preserving opacity, color, and state transitions that carry meaning. Reduced motion means fewer and gentler animations, not disabling all motion; feedback that confirms an action should remain legible. + ## Verify - The focal motion is specific to the selected world and surface. - Every supporting animation explains feedback, state, or relationship. - Interruption and repeated use behave correctly. - Desktop, mobile, and keyboard paths remain usable. +- The `prefers-reduced-motion` path reduces movement without erasing meaningful feedback or state changes. - Expensive effects stay smooth on the target device. - Removing an animation would lose meaning or authored character, not merely decoration. diff --git a/plugin/skills/impeccable/reference/animate.md b/plugin/skills/impeccable/reference/animate.md index d2e340763..4ae4cc5fc 100644 --- a/plugin/skills/impeccable/reference/animate.md +++ b/plugin/skills/impeccable/reference/animate.md @@ -74,12 +74,15 @@ Keep content visible in the default state so failed scripts do not hide the page Respect autoplay and sound preferences. Any nonessential loop must stop when offscreen or hidden. +Every web animation needs a `prefers-reduced-motion` path with an intentional alternative. Remove or reduce spatial movement while preserving opacity, color, and state transitions that carry meaning. Reduced motion means fewer and gentler animations, not disabling all motion; feedback that confirms an action should remain legible. + ## Verify - The focal motion is specific to the selected world and surface. - Every supporting animation explains feedback, state, or relationship. - Interruption and repeated use behave correctly. - Desktop, mobile, and keyboard paths remain usable. +- The `prefers-reduced-motion` path reduces movement without erasing meaningful feedback or state changes. - Expensive effects stay smooth on the target device. - Removing an animation would lose meaning or authored character, not merely decoration. From 477484aaee2eb47e5d973666363063a66f06c873 Mon Sep 17 00:00:00 2001 From: Abdul Wahab <32850166+abdulwahabone@users.noreply.github.com> Date: Sun, 9 Aug 2026 06:33:23 +0500 Subject: [PATCH 07/29] Fix: install missing explicitly selected providers without --force (#536) * Fix: install missing explicitly selected providers without --force (#500) An explicit --providers list now treats "already installed" per selected target: providers with an existing install take the update path, providers with none get a fresh install (skills + hooks) in the same run. Previously any existing install (e.g. .claude) made `install --providers=grok` exit 0 without writing .grok, leaving Grok Build on the Claude-variant fallback. Written with AI assistance (Cursor agent), reviewed and tested by maintainer. Co-authored-by: Cursor * Fix: copy provider agents for freshly installed mixed-install targets Bugbot caught that the mixed explicit-providers path installed skills and hooks for missing targets but skipped copyProviderAgents, which both the update and fresh-install paths run. Written with AI assistance (Cursor agent). Co-authored-by: Cursor --------- Co-authored-by: Cursor --- cli/bin/commands/skills.mjs | 36 +++++++++++++++++++------- tests/skills-cli.test.js | 51 +++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 9 deletions(-) diff --git a/cli/bin/commands/skills.mjs b/cli/bin/commands/skills.mjs index 5366e6375..741629027 100644 --- a/cli/bin/commands/skills.mjs +++ b/cli/bin/commands/skills.mjs @@ -1100,13 +1100,13 @@ async function chooseInstallScope(projectRoot, targets, detections, { yes, scope async function chooseInstallPlan(projectRoot, flags, { yes } = {}) { const providersValue = getFlagValue(flags, '--providers'); const scopeValue = getInstallScopeValue(flags); - const { targets, detections } = await chooseInstallProviders(projectRoot, providersValue, { yes }); + const { targets, detections, explicit } = await chooseInstallProviders(projectRoot, providersValue, { yes }); if (targets.length === 0) { throw new Error('Could not determine a target harness folder.'); } const scope = await chooseInstallScope(projectRoot, targets, detections, { yes, scopeValue }); const installRoot = installRootForScope(scope, projectRoot); - return { targets, scope, installRoot, hookRoot: projectRoot, detections }; + return { targets, scope, installRoot, hookRoot: projectRoot, detections, explicit }; } /** @@ -1812,16 +1812,23 @@ async function install(flags) { process.exit(1); } - const { targets, installRoot, hookRoot, scope } = plan; + const { targets, installRoot, hookRoot, scope, explicit } = plan; const existing = isAlreadyInstalled(installRoot, scope); + const installedTargets = existing ? findInstalledProviders(installRoot, scope) : []; + // An explicit --providers list is a per-target request: a selected provider + // with no install yet gets a fresh install instead of tripping the global + // "already installed" early exit (issue #500). When every selected provider + // is missing, skip the update branch entirely and take the fresh-install path. + const missingSelectedTargets = (existing && !force && explicit) + ? targets.filter(provider => !installedTargets.includes(provider)) + : []; - if (existing && !force) { + if (existing && !force && missingSelectedTargets.length < targets.length) { console.log(`Impeccable skills are already installed (found in ${existing}/).`); - const installedTargets = findInstalledProviders(installRoot, scope); const selectedInstalledTargets = targets.filter(provider => installedTargets.includes(provider)); const linkedTargets = findLinkedProviders(installRoot, selectedInstalledTargets, scope); const copyTargets = selectedInstalledTargets.filter(provider => !linkedTargets.includes(provider)); - const hookTargets = selectedInstalledTargets; + const hookTargets = [...selectedInstalledTargets, ...missingSelectedTargets]; const wantHooks = installHooks && await decideHookInstall(hookRoot, hookTargets, { yes }); let bundleDir; try { @@ -1836,11 +1843,11 @@ async function install(flags) { ? hookTargets.filter(provider => !hookInstalledForProvider(hookRoot, provider)) : []; let updateCheckSkipped = false; - if (copyTargets.length > 0 || missingHookTargets.length > 0) { + if (copyTargets.length > 0 || missingHookTargets.length > 0 || missingSelectedTargets.length > 0) { try { bundleDir = await downloadAndExtractBundle(); } catch (e) { - if (missingHookTargets.length > 0) throw e; + if (missingHookTargets.length > 0 || missingSelectedTargets.length > 0) throw e; updateCheckSkipped = true; console.log(`Could not check for skill updates: ${e.message}`); } @@ -1854,6 +1861,17 @@ async function install(flags) { console.log(`Updated ${updated} skill(s)${v ? ` to v${v}` : ''}.`); } + let freshWritten = 0; + if (!updateCheckSkipped && missingSelectedTargets.length > 0) { + freshWritten = copyProviderSkills(bundleDir, installRoot, missingSelectedTargets, { scope }); + if (freshWritten === 0) { + console.error(`Nothing was installed: the bundle had no variants for ${missingSelectedTargets.join(', ')}.`); + process.exit(1); + } + console.log(`Installed impeccable into: ${missingSelectedTargets.join(', ')} (${scope === 'user' ? 'global' : 'project'})`); + reportProviderAgents(copyProviderAgents(bundleDir, installRoot, missingSelectedTargets, { scope })); + } + const writtenHookTargets = missingHookTargets.length > 0 ? copyProviderHooks(bundleDir, hookRoot, missingHookTargets, { skillRoot: installRoot }) : []; @@ -1862,7 +1880,7 @@ async function install(flags) { if (updateCheckSkipped) { console.log('Existing skills were left unchanged.'); console.log('Run with --force to reinstall.\n'); - } else if (updated === 0 && writtenHookTargets.length === 0) { + } else if (updated === 0 && writtenHookTargets.length === 0 && freshWritten === 0) { const v = getSkillsVersion(installRoot, scope); console.log(`Skills are up to date${v ? ` (v${v})` : ''}.`); console.log('Run with --force to reinstall.\n'); diff --git a/tests/skills-cli.test.js b/tests/skills-cli.test.js index 86ecc63d8..a93423caf 100644 --- a/tests/skills-cli.test.js +++ b/tests/skills-cli.test.js @@ -1446,6 +1446,57 @@ describe('skills install/update: local universal bundle e2e', () => { rmSync(tmp, { recursive: true, force: true }); }, 15000); + test('explicit --providers installs a missing provider without --force (#500)', () => { + const tmp = mkdtempSync(join(tmpdir(), 'imp-test-explicit-missing-')); + execSync('git init', { cwd: tmp }); + const bundleRoot = createFakeUniversalBundle(tmp, ['.claude', '.cursor']); + + // Seed an existing .claude install; .cursor has nothing yet. + const skillDir = join(tmp, '.claude', 'skills', 'impeccable'); + mkdirSync(skillDir, { recursive: true }); + writeFileSync(join(skillDir, 'SKILL.md'), '---\nname: impeccable\nversion: 9.9.9-local\n---\nSeeded install.\n'); + + const output = run('skills install -y --providers=cursor --no-hooks', { + cwd: tmp, + env: { ...process.env, IMPECCABLE_BUNDLE_PATH: bundleRoot }, + }); + + expect(output).toContain('Installed impeccable into: .cursor'); + expect(readFileSync(join(tmp, '.cursor', 'skills', 'impeccable', 'SKILL.md'), 'utf8')).toContain('version: 9.9.9-local'); + // The unselected .claude install is left alone. + expect(readFileSync(join(skillDir, 'SKILL.md'), 'utf8')).toContain('Seeded install.'); + + rmSync(tmp, { recursive: true, force: true }); + }, 15000); + + test('explicit --providers mixes per-target updates and fresh installs (#500)', () => { + const tmp = mkdtempSync(join(tmpdir(), 'imp-test-explicit-mixed-')); + execSync('git init', { cwd: tmp }); + const bundleRoot = createFakeUniversalBundle(tmp, ['.claude', '.cursor']); + + // Stale .claude install; .cursor has nothing yet. + const skillDir = join(tmp, '.claude', 'skills', 'impeccable'); + mkdirSync(join(skillDir, 'scripts'), { recursive: true }); + writeFileSync(join(skillDir, 'SKILL.md'), '---\nname: impeccable\nstale: .claude\n---\nOld content.\n'); + writeFileSync(join(skillDir, 'scripts', 'context.mjs'), 'console.log("old script");\n'); + + const output = run('skills install -y --providers=claude,cursor', { + cwd: tmp, + env: { ...process.env, IMPECCABLE_BUNDLE_PATH: bundleRoot }, + }); + + expect(output).toContain('already installed'); + expect(output).toContain('Updated'); + expect(output).toContain('Installed impeccable into: .cursor'); + expect(readFileSync(join(skillDir, 'SKILL.md'), 'utf8')).toContain('version: 9.9.9-local'); + expect(readFileSync(join(tmp, '.cursor', 'skills', 'impeccable', 'SKILL.md'), 'utf8')).toContain('version: 9.9.9-local'); + // The freshly installed provider gets its hooks and agents too. + expect(existsSync(join(tmp, '.cursor', 'hooks.json'))).toBe(true); + expect(existsSync(join(tmp, '.cursor', 'agents', 'impeccable-finish-reviewer.md'))).toBe(true); + + rmSync(tmp, { recursive: true, force: true }); + }, 15000); + test('skills update --no-hooks refreshes skills without touching malformed hook manifests', () => { const tmp = mkdtempSync(join(tmpdir(), 'imp-test-update-no-hooks-')); execSync('git init', { cwd: tmp }); From 628aac5a400dd813ac9309e8a8db51effac3e6c9 Mon Sep 17 00:00:00 2001 From: Abdul Wahab <32850166+abdulwahabone@users.noreply.github.com> Date: Sun, 9 Aug 2026 06:40:26 +0500 Subject: [PATCH 08/29] Fix: point install's next step at the agent chat, not the terminal (#472) (#532) The install completion message said to run /impeccable init "in your AI harness", and users pasted it into their shell instead (bash: /impeccable: No such file or directory). Say the command is typed in the AI coding agent's chat, and give `npx impeccable init` a pointed redirect instead of the generic unknown-command error. A real path named `init` still routes to detect as before. Prepared with AI assistance (Cursor agent), directed by @abdulwahabone. Co-authored-by: Cursor --- cli/bin/cli.js | 6 ++++ cli/bin/commands/skills.mjs | 2 +- tests/skills-cli.test.js | 58 +++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/cli/bin/cli.js b/cli/bin/cli.js index 2f10dea69..16459e4e4 100755 --- a/cli/bin/cli.js +++ b/cli/bin/cli.js @@ -77,6 +77,12 @@ Compatibility: process.argv = [process.argv[0], process.argv[1], ...args]; const { detectCli } = await import('../engine/detect-antipatterns.mjs'); await detectCli(); + } else if (command === 'init') { + // The follow-up mistake from issue #472: `/impeccable init` belongs in an AI + // coding agent's chat, and a user who typed it into their shell is likely to + // retry it here as `npx impeccable init`. + console.error(`"init" is not a CLI command. Type /impeccable init in your AI coding agent's chat (Claude Code, Cursor, Codex, ...), not in this terminal.`); + process.exit(1); } else { // An unknown bareword: a mistyped command (or an old cached version run // against newer docs). Fail loudly instead of silently statting it as a path. diff --git a/cli/bin/commands/skills.mjs b/cli/bin/commands/skills.mjs index 741629027..03ce12454 100644 --- a/cli/bin/commands/skills.mjs +++ b/cli/bin/commands/skills.mjs @@ -1944,7 +1944,7 @@ async function install(flags) { reportProviderAgents(agentResults); if (hookTargets.length > 0) console.log(`Installed hooks into: ${hookTargets.join(', ')}`); - console.log('\nDone! Run /impeccable init in your AI harness to set up design context.\n'); + console.log('\nDone! Now type /impeccable init in your AI coding agent\'s chat (not in this terminal) to set up design context.\n'); } // ─── skills update ──────────────────────────────────────────────────────────── diff --git a/tests/skills-cli.test.js b/tests/skills-cli.test.js index a93423caf..c51ef7680 100644 --- a/tests/skills-cli.test.js +++ b/tests/skills-cli.test.js @@ -719,6 +719,64 @@ describe('skills install/update: local universal bundle e2e', () => { rmSync(home, { recursive: true, force: true }); }, 15000); + test('install completion says /impeccable init runs in the agent chat, not the terminal (#472)', () => { + const tmp = mkdtempSync(join(tmpdir(), 'imp-test-install-472-msg-')); + const home = mkdtempSync(join(tmpdir(), 'imp-home-install-472-msg-')); + execSync('git init', { cwd: tmp }); + const bundleRoot = createFakeUniversalBundle(tmp, ['.claude']); + + const output = run('install -y --providers=claude --no-hooks', { + cwd: tmp, + env: { ...process.env, HOME: home, IMPECCABLE_BUNDLE_PATH: bundleRoot }, + }); + + expect(output).toContain("type /impeccable init in your AI coding agent's chat (not in this terminal)"); + + rmSync(tmp, { recursive: true, force: true }); + rmSync(home, { recursive: true, force: true }); + }, 15000); + + test('`impeccable init` in the shell points at the agent chat instead of "Unknown command" (#472)', () => { + const tmp = mkdtempSync(join(tmpdir(), 'imp-test-init-472-')); + + let error; + try { + run('init', { cwd: tmp, stdio: 'pipe' }); + } catch (e) { + error = e; + } + + expect(error).toBeDefined(); + expect(error.status).toBe(1); + const stderr = String(error.stderr); + expect(stderr).toContain("Type /impeccable init in your AI coding agent's chat"); + expect(stderr).not.toContain('Unknown command'); + + rmSync(tmp, { recursive: true, force: true }); + }, 15000); + + test('a real path named init still routes to detect, not the #472 guidance', () => { + const tmp = mkdtempSync(join(tmpdir(), 'imp-test-init-path-472-')); + mkdirSync(join(tmp, 'init'), { recursive: true }); + writeFileSync(join(tmp, 'init', 'page.html'), 't

hello

\n'); + + // Detect exits 0 on a clean scan and 2 when findings surface; either way it + // must be the detector answering, not the init redirect. --json makes that + // positive: the detector always prints a JSON findings array. + let output = ''; + try { + output = run('init --json', { cwd: tmp, stdio: 'pipe' }); + } catch (e) { + output = `${e.stdout || ''}${e.stderr || ''}`; + } + + expect(output.trim().startsWith('[')).toBe(true); + expect(output).not.toContain('is not a CLI command'); + expect(output).not.toContain('Unknown command'); + + rmSync(tmp, { recursive: true, force: true }); + }, 60000); + test('formats detected harnesses as concise source-to-target rows', () => { const tmp = mkdtempSync(join(tmpdir(), 'imp-test-detect-lines-')); const home = mkdtempSync(join(tmpdir(), 'imp-home-detect-lines-')); From ddf4526fb504574606b08b98dcfcbe109258f7b0 Mon Sep 17 00:00:00 2001 From: Abdul Wahab <32850166+abdulwahabone@users.noreply.github.com> Date: Sun, 9 Aug 2026 06:41:19 +0500 Subject: [PATCH 09/29] Fix Windows libuv abort in concept-seed after a successful roll (#526) * Fix Windows libuv abort in concept-seed after a successful roll process.exit() with a live fetch keep-alive socket trips libuv's UV_HANDLE_CLOSING assertion on Windows (nodejs/node#56645), aborting the CLI with 0xC0000409 after complete output on the successful-roll path. Destroy the global undici dispatcher before the explicit exit so no socket is left to race; the hard exit stays, keeping the no-linger guarantee on blackholed networks. Fixes #504 Prepared with AI assistance (Cursor agent) under maintainer direction. * Add regression test for the successful-API dispatcher teardown The suite covered local rolls and the unreachable-API fallback but never a successful roll, the one path where a pooled keep-alive socket exists at exit (issue #504). Serve a real /api/roll from a local server and assert the CLI destroys fetch's global dispatcher before its explicit exit. Verified to fail without the fix. Prepared with AI assistance (Cursor agent) under maintainer direction. Co-authored-by: Cursor --------- Co-authored-by: Cursor --- skill/scripts/concept-seed.mjs | 9 +++- tests/concept-seed.test.mjs | 98 +++++++++++++++++++++++++++++++++- 2 files changed, 104 insertions(+), 3 deletions(-) diff --git a/skill/scripts/concept-seed.mjs b/skill/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/skill/scripts/concept-seed.mjs +++ b/skill/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/tests/concept-seed.test.mjs b/tests/concept-seed.test.mjs index e1963f84c..2991d57e2 100644 --- a/tests/concept-seed.test.mjs +++ b/tests/concept-seed.test.mjs @@ -1,10 +1,11 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; -import { spawnSync } from 'node:child_process'; +import { spawn, spawnSync } from 'node:child_process'; import { mkdtempSync, writeFileSync } from 'node:fs'; +import { createServer } from 'node:http'; import { tmpdir } from 'node:os'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import { readConceptCatalog, validateConceptCatalog, @@ -711,3 +712,96 @@ describe('init gate', () => { ); }); }); + +// The Windows abort in issue #504 (nodejs/node#56645) needs three things at +// once: a successful roll over Node's undici-backed fetch, the keep-alive +// socket that success leaves pooled, and the explicit process.exit at the end +// of the CLI. The suite's other API test exercises only the unreachable-API +// fallback, which leaves no pooled socket and so never walked the crashing +// path. This one serves a real roll from a local server and asserts the CLI +// destroys fetch's global dispatcher before exiting, so the teardown cannot +// silently regress. The teardown is Node fetch internals, so the CLI is +// spawned with node even when the suite itself runs under bun. +describe('API roll path', () => { + const NODE = process.versions.bun ? 'node' : process.execPath; + + const ROLL_PAYLOAD = { + poolRevision: 'api-test-rev', + approvedCount: 6, + catalogCount: 9, + challengers: [{ + id: 'api-test-world', + form: 'a letterpress print shop, where type, ink, and impression organize the page', + spark: 'Deep impressions hold the central promise while loose sorts wait in the case.', + system: ['Palette/material: dense ink black bitten into soft cotton paper'], + webLeverage: 'Variable-font impression depth with a keyboard-readable page structure', + }], + compositions: [], + }; + + // Wraps the global dispatcher's destroy so the parent test can observe the + // CLI's exit teardown. The warmup fetch makes fetch install the dispatcher + // before the wrap, and parks a keep-alive socket in its pool, which is the + // exact state the Windows crash needs at exit. + const PRELOAD = [ + "const KEY = Symbol.for('undici.globalDispatcher.1');", + 'await fetch(`${process.env.IMPECCABLE_API_URL}/warmup`).then(r => r.arrayBuffer()).catch(() => {});', + 'const dispatcher = globalThis[KEY];', + "if (dispatcher && typeof dispatcher.destroy === 'function') {", + ' const destroy = dispatcher.destroy.bind(dispatcher);', + ' dispatcher.destroy = (...args) => {', + " process.stderr.write('DISPATCHER_DESTROY_CALLED\\n');", + ' return destroy(...args);', + ' };', + '}', + '', + ].join('\n'); + + it('resolves a successful roll and destroys the fetch dispatcher before the explicit exit', async () => { + const requests = []; + const server = createServer((req, res) => { + requests.push(req.url); + if (req.url.startsWith('/api/roll?')) { + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(ROLL_PAYLOAD)); + return; + } + res.statusCode = 404; + res.end('not found'); + }); + await new Promise(resolveListen => server.listen(0, '127.0.0.1', resolveListen)); + try { + const dir = mkdtempSync(path.join(tmpdir(), 'concept-seed-api-')); + writeFileSync(path.join(dir, 'PRODUCT.md'), '# Test Product\n\n## Platform\n\nweb\n'); + const preloadPath = path.join(dir, 'wrap-dispatcher.mjs'); + writeFileSync(preloadPath, PRELOAD); + const result = await new Promise((resolveRun, rejectRun) => { + const child = spawn(NODE, [ + '--import', pathToFileURL(preloadPath).href, + SCRIPT, '--scope', 'direction', '--mode', 'persuade', '--from', 'api-test', + ], { + cwd: dir, + env: { + ...process.env, + IMPECCABLE_CATALOG_DIR: '/nonexistent-catalog-dir', + IMPECCABLE_API_URL: `http://127.0.0.1:${server.address().port}/api`, + }, + }); + let stdout = ''; + let stderr = ''; + child.stdout.on('data', chunk => { stdout += chunk; }); + child.stderr.on('data', chunk => { stderr += chunk; }); + child.on('error', rejectRun); + child.on('close', status => resolveRun({ status, stdout, stderr })); + }); + assert.equal(result.status, 0, `stderr: ${result.stderr}`); + assert.equal(requests.some(url => url.startsWith('/api/roll?')), true, 'the CLI must hit the roll endpoint'); + assert.match(result.stdout, /source: api/); + assert.match(result.stdout, /letterpress print shop/); + assert.match(result.stdout, /TELEMETRY:/); + assert.match(result.stderr, /DISPATCHER_DESTROY_CALLED/, 'the dispatcher must be destroyed before process.exit'); + } finally { + server.close(); + } + }); +}); From 4596f3183cf1ae32aa527e40b3d558f021a49bb1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 9 Aug 2026 01:41:51 +0000 Subject: [PATCH 10/29] Sync generated provider output --- .agents/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .claude/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .cursor/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .gemini/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .github/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .grok/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .kiro/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .opencode/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .pi/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .qoder/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .rovodev/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .trae-cn/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .trae/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .vibe/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- plugin/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- 15 files changed, 120 insertions(+), 15 deletions(-) diff --git a/.agents/skills/impeccable/scripts/concept-seed.mjs b/.agents/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.agents/skills/impeccable/scripts/concept-seed.mjs +++ b/.agents/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.claude/skills/impeccable/scripts/concept-seed.mjs b/.claude/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.claude/skills/impeccable/scripts/concept-seed.mjs +++ b/.claude/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.cursor/skills/impeccable/scripts/concept-seed.mjs b/.cursor/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.cursor/skills/impeccable/scripts/concept-seed.mjs +++ b/.cursor/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.gemini/skills/impeccable/scripts/concept-seed.mjs b/.gemini/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.gemini/skills/impeccable/scripts/concept-seed.mjs +++ b/.gemini/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.github/skills/impeccable/scripts/concept-seed.mjs b/.github/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.github/skills/impeccable/scripts/concept-seed.mjs +++ b/.github/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.grok/skills/impeccable/scripts/concept-seed.mjs b/.grok/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.grok/skills/impeccable/scripts/concept-seed.mjs +++ b/.grok/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.kiro/skills/impeccable/scripts/concept-seed.mjs b/.kiro/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.kiro/skills/impeccable/scripts/concept-seed.mjs +++ b/.kiro/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.opencode/skills/impeccable/scripts/concept-seed.mjs b/.opencode/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.opencode/skills/impeccable/scripts/concept-seed.mjs +++ b/.opencode/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.pi/skills/impeccable/scripts/concept-seed.mjs b/.pi/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.pi/skills/impeccable/scripts/concept-seed.mjs +++ b/.pi/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.qoder/skills/impeccable/scripts/concept-seed.mjs b/.qoder/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.qoder/skills/impeccable/scripts/concept-seed.mjs +++ b/.qoder/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.rovodev/skills/impeccable/scripts/concept-seed.mjs b/.rovodev/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.rovodev/skills/impeccable/scripts/concept-seed.mjs +++ b/.rovodev/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.trae-cn/skills/impeccable/scripts/concept-seed.mjs b/.trae-cn/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.trae-cn/skills/impeccable/scripts/concept-seed.mjs +++ b/.trae-cn/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.trae/skills/impeccable/scripts/concept-seed.mjs b/.trae/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.trae/skills/impeccable/scripts/concept-seed.mjs +++ b/.trae/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.vibe/skills/impeccable/scripts/concept-seed.mjs b/.vibe/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.vibe/skills/impeccable/scripts/concept-seed.mjs +++ b/.vibe/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/plugin/skills/impeccable/scripts/concept-seed.mjs b/plugin/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/plugin/skills/impeccable/scripts/concept-seed.mjs +++ b/plugin/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } From 490dcfd6783b25b2068bc7d95e7687e46099816a Mon Sep 17 00:00:00 2001 From: Abdul Wahab <32850166+abdulwahabone@users.noreply.github.com> Date: Sun, 9 Aug 2026 06:43:17 +0500 Subject: [PATCH 11/29] Fix #476: stop using JSON.stringify/double quotes as shell quoting in four exec sites (#533) * Fix: use argv exec and single-quote escaping for the four #476 shell-injection sites JSON.stringify and raw double-quote interpolation were used as shell quoting, but /bin/sh still expands $(...), backticks, and ${} inside double quotes. - is-generated.mjs / live.mjs runScript: switch execSync string commands to execFileSync argv form, which never invokes a shell. Closes the remote path where a source file named `$(...)` executes during the live-mode walk. - skills.mjs hook command + hook-lib.mjs ignore-value suggestion: values that must stay shell strings now use POSIX single-quote escaping instead of JSON/double quotes. The doctor's hook-token parser learns the single-quoted absolute form so it keeps verifying user-level installs. Adds regression tests for the single-quoted absolute hook form and the single-quoted ignore-value suggestion. Verified end to end in a browser through a real live-mode wrap walk against a hostile-named source file. Prepared with AI assistance (Cursor) under maintainer instruction. Co-authored-by: Cursor * Test: lock in POSIX single-quoting for a $(...) absolute install path (#476) Follow-up from security review: prove an install path embedding $(...) is single-quoted in the written hook manifest, not double-quoted. Prepared with AI assistance (Cursor) under maintainer instruction. Co-authored-by: Cursor * Fix: quote ignore-command args per platform so Windows cmd.exe keeps spaces (#533) Greptile flagged that switching quoteCommandArg to POSIX single quotes fixed $(...) injection on /bin/sh but regressed Windows cmd.exe, where single quotes are literal, so a --file path containing spaces was split and the ignore scope was stored malformed. The suggested command runs on the same machine the hook fired on, so branch on process.platform (the pattern skills.mjs already uses): single-quote on POSIX for the #476 fix, and keep the original double-quote escaping on Windows so that path's behavior is unchanged. Adds a regression test asserting both forms. Prepared with AI assistance (Cursor) under maintainer instruction. Co-authored-by: Cursor * Test: prove the POSIX hook guard is inert under /bin/sh and Windows keeps double quotes (#533) Greptile's probe could not reach the generated manifest, leaving the hook command contract unverified. Convert that into committed proof: - POSIX: install with a $(touch pwned) absolute path, then actually execute the generated guard under /bin/sh from a clean cwd and assert no marker file appears and the guard exits 0 (single-quoted substitution stays inert). - Windows: drive copyProviderHooks as win32 in-process and assert the command keeps the double-quoted absolute path (usable when the install path has spaces; $(...) is inert on cmd.exe anyway). Test-only; source quoting is unchanged. Prepared with AI assistance (Cursor) under maintainer instruction. Co-authored-by: Cursor --------- Co-authored-by: Cursor --- cli/bin/commands/skills.mjs | 26 ++++++++--- skill/scripts/hook-lib.mjs | 14 +++++- skill/scripts/lib/is-generated.mjs | 7 ++- skill/scripts/lib/staleness-deep.mjs | 9 +++- skill/scripts/live.mjs | 14 ++++-- tests/doctor.test.mjs | 23 ++++++++++ tests/hook.test.mjs | 36 +++++++++++++++ tests/skills-cli.test.js | 67 +++++++++++++++++++++++++++- 8 files changed, 182 insertions(+), 14 deletions(-) diff --git a/cli/bin/commands/skills.mjs b/cli/bin/commands/skills.mjs index 03ce12454..c57c4d0a6 100644 --- a/cli/bin/commands/skills.mjs +++ b/cli/bin/commands/skills.mjs @@ -1365,17 +1365,28 @@ function hookScriptPathForProvider(skillRoot, provider) { // with single quotes for the inner string literals. const WIN32_HOOK_GUARD_SCRIPT = "const p=process.argv[1];const f=require('fs');if(f.existsSync(p)){const r=require('child_process').spawnSync(process.execPath,[p],{stdio:'inherit'});process.exit(r.status===null?1:r.status);}"; +// POSIX single-quote escaping. JSON.stringify is not shell quoting: inside +// double quotes /bin/sh still expands $(...), backticks, and ${}, and this +// string is baked into a hook manifest the harness re-executes on every edit, +// so an install path embedding $(...) would run it repeatedly (issue #476). +// Windows command forms keep double quotes: cmd.exe treats ' as a literal +// character and performs no command substitution. +function shSingleQuote(value) { + return `'${String(value).replace(/'/g, `'\\''`)}'`; +} + function windowsHookCommand(quotedPath) { return `if exist ${quotedPath} (node ${quotedPath} & exit /b)`; } +// `quotedPath` carries one pre-quoted form per target shell: { posix, win32 }. function guardHookCommand(quotedPath, provider) { // `.agents` (Codex) keeps the POSIX form unconditionally: its Windows // consumers read the commandWindows sibling instead. if (provider !== '.agents' && process.platform === 'win32') { - return `node -e "${WIN32_HOOK_GUARD_SCRIPT}" ${quotedPath}`; + return `node -e "${WIN32_HOOK_GUARD_SCRIPT}" ${quotedPath.win32}`; } - return `[ ! -f ${quotedPath} ] || node ${quotedPath}`; + return `[ ! -f ${quotedPath.posix} ] || node ${quotedPath.posix}`; } // Transform bundled hook commands for the actual install target: @@ -1398,9 +1409,14 @@ function rewriteHookCommandsForSkillRoot(value, provider, { skillRoot, absolute // Project-scope installs derive the provider's own project-relative path // rather than trusting the bundle token, which for Codex points at // `.codex/skills/...` while the CLI installs the skill at `.agents/skills/`. + // The absolute path comes from the install root (project dir or $HOME), so + // its POSIX form gets real single-quote escaping (issue #476). The relative + // form is a per-provider constant and stays double-quoted, because Claude's + // ${CLAUDE_PROJECT_DIR} token must keep expanding at hook time. + const relPath = hookScriptRelPathForProvider(provider); const quotedPath = absolute - ? JSON.stringify(hookScript) - : JSON.stringify(hookScriptRelPathForProvider(provider)); + ? { posix: shSingleQuote(hookScript), win32: JSON.stringify(hookScript) } + : { posix: JSON.stringify(relPath), win32: JSON.stringify(relPath) }; if (typeof value === 'string') { if (!valueHasImpeccableHookMarker(value)) return value; @@ -1415,7 +1431,7 @@ function rewriteHookCommandsForSkillRoot(value, provider, { skillRoot, absolute next[key] = rewriteHookCommandsForSkillRoot(child, provider, { skillRoot, absolute }); } if (provider === '.agents' && typeof value.command === 'string' && valueHasImpeccableHookMarker(value.command)) { - next.commandWindows = windowsHookCommand(quotedPath); + next.commandWindows = windowsHookCommand(quotedPath.win32); } return next; } diff --git a/skill/scripts/hook-lib.mjs b/skill/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/skill/scripts/hook-lib.mjs +++ b/skill/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/skill/scripts/lib/is-generated.mjs b/skill/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/skill/scripts/lib/is-generated.mjs +++ b/skill/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/skill/scripts/lib/staleness-deep.mjs b/skill/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/skill/scripts/lib/staleness-deep.mjs +++ b/skill/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/skill/scripts/live.mjs b/skill/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/skill/scripts/live.mjs +++ b/skill/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/tests/doctor.test.mjs b/tests/doctor.test.mjs index 7d801dfb8..877c38b0e 100644 --- a/tests/doctor.test.mjs +++ b/tests/doctor.test.mjs @@ -486,6 +486,29 @@ describe('checkHookInstallation', () => { ); }); + it('handles the #476 single-quoted absolute form (user-level installs)', () => { + // The shell-injection fix single-quotes the absolute POSIX path instead of + // JSON.stringify. The doctor's token parser must read the single-quoted + // form too, or it silently stops verifying every user-level install. + const abs = path.join(scratch, '.claude', 'skills', 'impeccable', 'scripts', 'hook.mjs'); + const p = `'${abs}'`; + const guarded = `[ ! -f ${p} ] || node ${p}`; + write('.claude/settings.json', JSON.stringify({ + hooks: { Stop: [{ hooks: [{ command: guarded }] }] }, + })); + // absolute path missing → flagged + assert.deepEqual( + ids(checkHookInstallation({ projectRoot: scratch, repoRoot: scratch, providerId: 'claude-code' })), + ['hook-script-missing'], + ); + // present → quiet + write('.claude/skills/impeccable/scripts/hook.mjs', '// hook\n'); + assert.deepEqual( + checkHookInstallation({ projectRoot: scratch, repoRoot: scratch, providerId: 'claude-code' }), + [], + ); + }); + it('never reports missing for the GitHub $(git rev-parse) form', () => { // Command substitution is not statically resolvable; a doctor must not // assert a negative it cannot verify. diff --git a/tests/hook.test.mjs b/tests/hook.test.mjs index 631fc245d..d4c9589db 100644 --- a/tests/hook.test.mjs +++ b/tests/hook.test.mjs @@ -1142,6 +1142,42 @@ describe('renderTemplate()', () => { assert.match(text, /\/impeccable hooks ignore-value bounce-easing bounce-ball --shared/); }); + it('single-quotes a hostile font value so the suggestion cannot inject a shell command (#476)', () => { + // The suggested command comes straight from scanned file content. A + // double-quoted arg would leave $(...) live for whoever runs the + // suggestion; single quotes neutralize it. + const text = renderTemplate( + [finding('overused-font', 1, { + name: 'Overused font', + snippet: 'body { font-family: "$(touch pwned)", sans-serif; }', + })], + '/x/fonts.css', DEFAULT_CONFIG, { cwd: '/x' } + ); + assert.match(text, /ignore-value overused-font '\$\(touch pwned\)' --shared/); + assert.doesNotMatch(text, /ignore-value overused-font "\$\(touch pwned\)"/); + }); + + it('quotes the --file path per platform: single quotes on POSIX, double quotes on Windows (#533)', () => { + // The suggested command is run on the same machine the hook fired on. + // POSIX needs single quotes so $(...) in a filename cannot execute; Windows + // cmd.exe treats single quotes as literal, so a path with spaces must stay + // double-quoted or the ignore scope is split at the space. + const original = process.platform; + const renderFor = (platform) => { + Object.defineProperty(process, 'platform', { value: platform, configurable: true }); + try { + return renderTemplate( + [finding('side-tab', 1, { name: 'Side tab' })], + '/x/My Components/Card.tsx', DEFAULT_CONFIG, { cwd: '/x' } + ); + } finally { + Object.defineProperty(process, 'platform', { value: original, configurable: true }); + } + }; + assert.match(renderFor('linux'), /--file 'My Components\/Card\.tsx'/); + assert.match(renderFor('win32'), /--file "My Components\/Card\.tsx"/); + }); + it('drops the L prefix when line is 0', () => { const text = renderTemplate( [finding('side-tab', 0, { name: 'X' })], diff --git a/tests/skills-cli.test.js b/tests/skills-cli.test.js index c51ef7680..427f33e25 100644 --- a/tests/skills-cli.test.js +++ b/tests/skills-cli.test.js @@ -10,7 +10,7 @@ * gracefully when impeccable.style is unreachable. */ import { describe, test, expect, beforeAll, afterAll } from 'bun:test'; -import { execSync } from 'child_process'; +import { execSync, execFileSync } from 'child_process'; import { mkdtempSync, existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, rmSync, lstatSync, realpathSync, readlinkSync, symlinkSync } from 'fs'; import { join } from 'path'; import { tmpdir } from 'os'; @@ -1728,6 +1728,71 @@ describe('copyProviderHooks: hook command path resolution (#399)', () => { rmSync(tmp, { recursive: true, force: true }); rmSync(skillHome, { recursive: true, force: true }); }); + + test('single-quotes an absolute install path that embeds $(...), and the guard is inert under /bin/sh (#476)', () => { + // A hook command is re-executed by the harness on every edit. JSON.stringify + // is not shell quoting: an install path containing $(...) inside double + // quotes would run on each fire. The absolute POSIX form must be + // single-quoted so the substitution stays inert. + const tmp = mkdtempSync(join(tmpdir(), 'imp-hook-split-')); + const skillHome = mkdtempSync(join(tmpdir(), 'imp-hook-$(touch pwned)-')); + const bundleDir = createProjectDirBundle(tmp); + + copyProviderHooks(bundleDir, tmp, ['.claude'], { skillRoot: skillHome }); + + const raw = readFileSync(join(tmp, '.claude', 'settings.local.json'), 'utf8'); + // The path appears single-quoted, never double-quoted (which would leave + // the substitution live for /bin/sh). + expect(raw).toContain(`'${skillHome}`); + expect(raw).not.toContain(`"${skillHome}`); + + const commands = claudeHookCommands(join(tmp, '.claude', 'settings.local.json')); + expect(commands.length).toBeGreaterThan(0); + // End-to-end: actually run each generated guard under /bin/sh from a clean + // cwd. The hook script does not exist (skillHome is empty), so `[ ! -f ... ]` + // short-circuits and node never runs — and crucially the single-quoted + // $(touch pwned) must not execute. Prove it: no `pwned` file appears and the + // guard exits 0. + if (process.platform !== 'win32') { + const runCwd = mkdtempSync(join(tmpdir(), 'imp-hook-run-')); + for (const command of commands) { + expect(command).toContain('[ ! -f '); + expect(command).not.toMatch(/"[^"]*\$\(touch pwned\)/); + execFileSync('/bin/sh', ['-c', command], { cwd: runCwd, stdio: 'ignore' }); + } + expect(existsSync(join(runCwd, 'pwned'))).toBe(false); + rmSync(runCwd, { recursive: true, force: true }); + } + rmSync(tmp, { recursive: true, force: true }); + rmSync(skillHome, { recursive: true, force: true }); + }); + + test('the Windows hook form keeps a usable double-quoted absolute path (#533)', () => { + // cmd.exe does no $(...) substitution but treats single quotes as literal, + // so the Windows command form must keep the absolute path double-quoted or + // a space in the install path would split the argument. copyProviderHooks + // branches on process.platform, so drive it as win32 in-process. + const original = process.platform; + const tmp = mkdtempSync(join(tmpdir(), 'imp-hook-win-')); + const skillHome = mkdtempSync(join(tmpdir(), 'imp-hook-win-home-')); + const bundleDir = createProjectDirBundle(tmp); + try { + Object.defineProperty(process, 'platform', { value: 'win32', configurable: true }); + copyProviderHooks(bundleDir, tmp, ['.claude'], { skillRoot: skillHome }); + } finally { + Object.defineProperty(process, 'platform', { value: original, configurable: true }); + } + + const absolute = join(skillHome, '.claude', 'skills', 'impeccable', 'scripts', 'hook.mjs'); + for (const command of claudeHookCommands(join(tmp, '.claude', 'settings.local.json'))) { + // Windows guard shape (node -e wrapper) with the absolute path double-quoted. + expect(command).toContain(`"${absolute}"`); + expect(command).not.toContain(`'${absolute}`); + expect(command).toContain('node -e'); + } + rmSync(tmp, { recursive: true, force: true }); + rmSync(skillHome, { recursive: true, force: true }); + }); }); // ─── Update scope resolution (issue #399, part 2) ──────────────────────────── From 5c8652b01994d12caadc6e4416496c29b669b3a9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 9 Aug 2026 01:43:56 +0000 Subject: [PATCH 12/29] Sync generated provider output --- .agents/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .agents/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .claude/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .claude/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .cursor/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .cursor/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .gemini/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .gemini/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .github/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .github/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .grok/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .grok/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .kiro/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .kiro/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .opencode/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .opencode/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .pi/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .pi/skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .pi/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .qoder/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .qoder/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .rovodev/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .rovodev/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .trae-cn/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .trae-cn/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .trae/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .trae/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .vibe/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .vibe/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- plugin/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- plugin/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- 60 files changed, 540 insertions(+), 120 deletions(-) diff --git a/.agents/skills/impeccable/scripts/hook-lib.mjs b/.agents/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.agents/skills/impeccable/scripts/hook-lib.mjs +++ b/.agents/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.agents/skills/impeccable/scripts/lib/is-generated.mjs b/.agents/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.agents/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.agents/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.agents/skills/impeccable/scripts/lib/staleness-deep.mjs b/.agents/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.agents/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.agents/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.agents/skills/impeccable/scripts/live.mjs b/.agents/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.agents/skills/impeccable/scripts/live.mjs +++ b/.agents/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.claude/skills/impeccable/scripts/hook-lib.mjs b/.claude/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.claude/skills/impeccable/scripts/hook-lib.mjs +++ b/.claude/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.claude/skills/impeccable/scripts/lib/is-generated.mjs b/.claude/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.claude/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.claude/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.claude/skills/impeccable/scripts/lib/staleness-deep.mjs b/.claude/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.claude/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.claude/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.claude/skills/impeccable/scripts/live.mjs b/.claude/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.claude/skills/impeccable/scripts/live.mjs +++ b/.claude/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.cursor/skills/impeccable/scripts/hook-lib.mjs b/.cursor/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.cursor/skills/impeccable/scripts/hook-lib.mjs +++ b/.cursor/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.cursor/skills/impeccable/scripts/lib/is-generated.mjs b/.cursor/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.cursor/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.cursor/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.cursor/skills/impeccable/scripts/lib/staleness-deep.mjs b/.cursor/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.cursor/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.cursor/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.cursor/skills/impeccable/scripts/live.mjs b/.cursor/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.cursor/skills/impeccable/scripts/live.mjs +++ b/.cursor/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.gemini/skills/impeccable/scripts/hook-lib.mjs b/.gemini/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.gemini/skills/impeccable/scripts/hook-lib.mjs +++ b/.gemini/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.gemini/skills/impeccable/scripts/lib/is-generated.mjs b/.gemini/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.gemini/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.gemini/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.gemini/skills/impeccable/scripts/lib/staleness-deep.mjs b/.gemini/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.gemini/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.gemini/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.gemini/skills/impeccable/scripts/live.mjs b/.gemini/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.gemini/skills/impeccable/scripts/live.mjs +++ b/.gemini/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.github/skills/impeccable/scripts/hook-lib.mjs b/.github/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.github/skills/impeccable/scripts/hook-lib.mjs +++ b/.github/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.github/skills/impeccable/scripts/lib/is-generated.mjs b/.github/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.github/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.github/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.github/skills/impeccable/scripts/lib/staleness-deep.mjs b/.github/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.github/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.github/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.github/skills/impeccable/scripts/live.mjs b/.github/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.github/skills/impeccable/scripts/live.mjs +++ b/.github/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.grok/skills/impeccable/scripts/hook-lib.mjs b/.grok/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.grok/skills/impeccable/scripts/hook-lib.mjs +++ b/.grok/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.grok/skills/impeccable/scripts/lib/is-generated.mjs b/.grok/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.grok/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.grok/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.grok/skills/impeccable/scripts/lib/staleness-deep.mjs b/.grok/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.grok/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.grok/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.grok/skills/impeccable/scripts/live.mjs b/.grok/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.grok/skills/impeccable/scripts/live.mjs +++ b/.grok/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.kiro/skills/impeccable/scripts/hook-lib.mjs b/.kiro/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.kiro/skills/impeccable/scripts/hook-lib.mjs +++ b/.kiro/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.kiro/skills/impeccable/scripts/lib/is-generated.mjs b/.kiro/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.kiro/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.kiro/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.kiro/skills/impeccable/scripts/lib/staleness-deep.mjs b/.kiro/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.kiro/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.kiro/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.kiro/skills/impeccable/scripts/live.mjs b/.kiro/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.kiro/skills/impeccable/scripts/live.mjs +++ b/.kiro/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.opencode/skills/impeccable/scripts/hook-lib.mjs b/.opencode/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.opencode/skills/impeccable/scripts/hook-lib.mjs +++ b/.opencode/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.opencode/skills/impeccable/scripts/lib/is-generated.mjs b/.opencode/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.opencode/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.opencode/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.opencode/skills/impeccable/scripts/lib/staleness-deep.mjs b/.opencode/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.opencode/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.opencode/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.opencode/skills/impeccable/scripts/live.mjs b/.opencode/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.opencode/skills/impeccable/scripts/live.mjs +++ b/.opencode/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.pi/skills/impeccable/scripts/hook-lib.mjs b/.pi/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.pi/skills/impeccable/scripts/hook-lib.mjs +++ b/.pi/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.pi/skills/impeccable/scripts/lib/is-generated.mjs b/.pi/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.pi/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.pi/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.pi/skills/impeccable/scripts/lib/staleness-deep.mjs b/.pi/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.pi/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.pi/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.pi/skills/impeccable/scripts/live.mjs b/.pi/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.pi/skills/impeccable/scripts/live.mjs +++ b/.pi/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.qoder/skills/impeccable/scripts/hook-lib.mjs b/.qoder/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.qoder/skills/impeccable/scripts/hook-lib.mjs +++ b/.qoder/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.qoder/skills/impeccable/scripts/lib/is-generated.mjs b/.qoder/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.qoder/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.qoder/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.qoder/skills/impeccable/scripts/lib/staleness-deep.mjs b/.qoder/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.qoder/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.qoder/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.qoder/skills/impeccable/scripts/live.mjs b/.qoder/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.qoder/skills/impeccable/scripts/live.mjs +++ b/.qoder/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.rovodev/skills/impeccable/scripts/hook-lib.mjs b/.rovodev/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.rovodev/skills/impeccable/scripts/hook-lib.mjs +++ b/.rovodev/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.rovodev/skills/impeccable/scripts/lib/is-generated.mjs b/.rovodev/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.rovodev/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.rovodev/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.rovodev/skills/impeccable/scripts/lib/staleness-deep.mjs b/.rovodev/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.rovodev/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.rovodev/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.rovodev/skills/impeccable/scripts/live.mjs b/.rovodev/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.rovodev/skills/impeccable/scripts/live.mjs +++ b/.rovodev/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.trae-cn/skills/impeccable/scripts/hook-lib.mjs b/.trae-cn/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.trae-cn/skills/impeccable/scripts/hook-lib.mjs +++ b/.trae-cn/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.trae-cn/skills/impeccable/scripts/lib/is-generated.mjs b/.trae-cn/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.trae-cn/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.trae-cn/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.trae-cn/skills/impeccable/scripts/lib/staleness-deep.mjs b/.trae-cn/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.trae-cn/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.trae-cn/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.trae-cn/skills/impeccable/scripts/live.mjs b/.trae-cn/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.trae-cn/skills/impeccable/scripts/live.mjs +++ b/.trae-cn/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.trae/skills/impeccable/scripts/hook-lib.mjs b/.trae/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.trae/skills/impeccable/scripts/hook-lib.mjs +++ b/.trae/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.trae/skills/impeccable/scripts/lib/is-generated.mjs b/.trae/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.trae/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.trae/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.trae/skills/impeccable/scripts/lib/staleness-deep.mjs b/.trae/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.trae/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.trae/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.trae/skills/impeccable/scripts/live.mjs b/.trae/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.trae/skills/impeccable/scripts/live.mjs +++ b/.trae/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.vibe/skills/impeccable/scripts/hook-lib.mjs b/.vibe/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.vibe/skills/impeccable/scripts/hook-lib.mjs +++ b/.vibe/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.vibe/skills/impeccable/scripts/lib/is-generated.mjs b/.vibe/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.vibe/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.vibe/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.vibe/skills/impeccable/scripts/lib/staleness-deep.mjs b/.vibe/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.vibe/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.vibe/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.vibe/skills/impeccable/scripts/live.mjs b/.vibe/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.vibe/skills/impeccable/scripts/live.mjs +++ b/.vibe/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/plugin/skills/impeccable/scripts/hook-lib.mjs b/plugin/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/plugin/skills/impeccable/scripts/hook-lib.mjs +++ b/plugin/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/plugin/skills/impeccable/scripts/lib/is-generated.mjs b/plugin/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/plugin/skills/impeccable/scripts/lib/is-generated.mjs +++ b/plugin/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/plugin/skills/impeccable/scripts/lib/staleness-deep.mjs b/plugin/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/plugin/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/plugin/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/plugin/skills/impeccable/scripts/live.mjs b/plugin/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/plugin/skills/impeccable/scripts/live.mjs +++ b/plugin/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } From 045865918afca96899a4aee2981b66bff6883a00 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Sat, 8 Aug 2026 19:26:43 -0700 Subject: [PATCH 13/29] Held for review: agent placeholder substitution, reviewer recapture contract, base-directory script form (#544) * Resolve {{scripts_path}} in the agent bodies Codex ships Three code paths emit an agent body: the degraded fallback reference, the .toml nested inside the skill for Codex, and the native agent file. Only the nested .toml skipped placeholder substitution and rule-marker stripping, so the codex and .agents dists shipped `node {{scripts_path}}/embed-prompt.mjs` verbatim in the asset producer, and every caller had to substitute the token itself at load time. All three now render through renderAgentBody(), and the new regression test asserts a runnable embed-prompt command on each emitted surface plus a synthetic agent proving markers and placeholders resolve in the nested .toml. Prepared by an AI agent (Claude Code) under pbakaus's instruction. Co-Authored-By: Claude Fable 5 * Give the finish reviewer's screenshots one fixed address The Input Contract asked for "desktop and mobile screenshot paths captured by the parent" and named none, so each session invented a filename and the verdict pass went looking for a recapture that was never written there. Two reviewer passes burned on that in the eval runs. The parent now captures and recaptures to .impeccable/review/desktop.png and .impeccable/review/mobile.png, and the reviewer reads those two first, treating a brief-named path as the fallback for a parent that wrote elsewhere. Prepared by an AI agent (Claude Code) under pbakaus's instruction. Co-Authored-By: Claude Fable 5 * Lead Setup with the base directory the runtime reports The rendered claude and codex skills opened with `node .claude/skills/impeccable/scripts/context.mjs`, a project-relative path that resolves in this repo and in nothing a user installs: a personal or plugin install puts the scripts outside the project entirely. The working form was already in the text, parenthesized, after the one that fails. Setup now leads with `node /scripts/context.mjs` and says once that the base directory resolves every scripts-path command in the skill and its references, leaving the project-relative path as the fallback for runtimes that report no base directory. Prepared by an AI agent (Claude Code) under pbakaus's instruction. Co-Authored-By: Claude Fable 5 * Answer the Copilot review: brittle model assertion, missing review dir Assert that {{model}} resolved rather than that it resolved to "GPT", which belongs to PROVIDER_PLACEHOLDERS and can change without touching what the test guards. And have the parent create .impeccable/review/ when the harness does not, so a fresh project's first capture has somewhere to land. Prepared by an AI agent (Claude Code) under pbakaus's instruction. Co-Authored-By: Claude Fable 5 * Make the review-screenshot contract directory-based, not web-viewport-named Two amendments to the recapture contract from review feedback: 1. The canonical location is the directory .impeccable/review/, one file per captured viewport; desktop.png and mobile.png are the web case, not the contract. Baking web-viewport names into the reviewer's spec would have hardened a web assumption into paths that a native (ios/android/adaptive) build cannot honestly write. 2. Precedence restored to explicit-beats-convention: paths the calling brief names are authoritative when the files exist; the canonical directory is where the reviewer looks when the brief names none or a named path is missing. This avoids stale canonical files from an earlier run silently winning over fresh explicit paths. The observed failure (the verdict round inventing a round-stamped filename) stays fixed: recapture happens over the same files, and invented filenames are still called out as pointing at nothing. Assisted-by: Claude Code --------- Co-authored-by: Claude Fable 5 --- scripts/lib/transformers/factory.js | 29 +++++--- skill/SKILL.src.md | 2 +- skill/agents/impeccable-finish-reviewer.md | 4 +- skill/reference/new-work.md | 2 +- tests/build.test.js | 84 ++++++++++++++++++++++ 5 files changed, 107 insertions(+), 14 deletions(-) diff --git a/scripts/lib/transformers/factory.js b/scripts/lib/transformers/factory.js index bf792d8bc..195e1b159 100644 --- a/scripts/lib/transformers/factory.js +++ b/scripts/lib/transformers/factory.js @@ -176,6 +176,22 @@ function buildCursorAgent(agent, body) { return `${generateYamlFrontmatter(frontmatter)}\n${body.trim()}\n`; } +/** + * Render an agent's markdown body for one provider. + * + * Every surface that ships an agent body (the degraded fallback reference, the + * Codex .toml nested inside the skill, and the native agent file) goes through + * here, so all three resolve provider blocks, {{placeholders}}, rule markers, + * and {{scripts_path}} the same way. The nested Codex .toml used to skip the + * last two and shipped `node {{scripts_path}}/embed-prompt.mjs` literally. + */ +function renderAgentBody(agent, { providerTags, placeholderKey, allSkillNames, scriptsPath }) { + let body = compileProviderBlocks(agent.body, providerTags); + body = replacePlaceholders(body, placeholderKey, [], allSkillNames); + body = stripRuleMarkers(body); + return body.replace(/\{\{scripts_path\}\}/g, scriptsPath); +} + function buildAgentFile(config, agent, body) { if (config.agentFormat === 'codex-toml') { return { @@ -330,10 +346,7 @@ export function createTransformer(config) { ensureDir(degradedDir); for (const agent of skill.agents) { const role = agent.name.replace(/^impeccable-/, ''); - let body = compileProviderBlocks(agent.body, providerTags); - body = replacePlaceholders(body, placeholderKey, [], allSkillNames); - body = stripRuleMarkers(body); - body = body.replace(/\{\{scripts_path\}\}/g, scriptsPath); + const body = renderAgentBody(agent, { providerTags, placeholderKey, allSkillNames, scriptsPath }); const content = `${DEGRADED_PREAMBLE}\n\n${body.replace(/^\s+/, '')}`; writeFile(path.join(degradedDir, `${role}.md`), content); refCount++; @@ -358,8 +371,7 @@ export function createTransformer(config) { if (CODEX_SKILL_PROVIDERS.has(provider)) { for (const agent of skill.agents || []) { if (agent.providers && !agent.providers.includes('codex')) continue; - let agentBody = compileProviderBlocks(agent.body, providerTags); - agentBody = replacePlaceholders(agentBody, placeholderKey, [], allSkillNames); + const agentBody = renderAgentBody(agent, { providerTags, placeholderKey, allSkillNames, scriptsPath }); const filename = `${agent.codexName || agent.name.replace(/-/g, '_')}.toml`; ensureDir(path.join(skillDir, 'agents')); writeFile(path.join(skillDir, 'agents', filename), buildCodexAgent(agent, agentBody)); @@ -375,10 +387,7 @@ export function createTransformer(config) { // Agents can declare `providers: ` to limit which harnesses // they emit to. Default (no field) ships everywhere with agentFormat. if (agent.providers && !agent.providers.includes(provider)) continue; - let body = compileProviderBlocks(agent.body, providerTags); - body = replacePlaceholders(body, placeholderKey, [], allSkillNames); - body = stripRuleMarkers(body); - body = body.replace(/\{\{scripts_path\}\}/g, scriptsPath); + const body = renderAgentBody(agent, { providerTags, placeholderKey, allSkillNames, scriptsPath }); const agentFile = buildAgentFile(config, agent, body); if (!agentFile) continue; ensureDir(agentsDir); diff --git a/skill/SKILL.src.md b/skill/SKILL.src.md index 59e56f09c..004a1b03e 100644 --- a/skill/SKILL.src.md +++ b/skill/SKILL.src.md @@ -18,7 +18,7 @@ Core principles: ## Setup -1. Run `node {{scripts_path}}/context.mjs` once per session (if the runtime shows this skill's loaded base directory, run `node /scripts/context.mjs`; keep cwd at the user's project). Pass a named source file or route as `--target `. It loads PRODUCT.md, DESIGN.md, the matching surface brief, and native-platform guidance when applicable; follow its directives and do not rerun it. +1. Run `node /scripts/context.mjs` once per session, where `` is the loaded base directory the runtime reports for this skill; keep cwd at the user's project. That base directory resolves every `node {{scripts_path}}/...` command in this skill and its references, and `{{scripts_path}}` is the fallback only when the runtime reports no base directory. Pass a named source file or route as `--target `. It loads PRODUCT.md, DESIGN.md, the matching surface brief, and native-platform guidance when applicable; follow its directives and do not rerun it. 2. Before acting, load the one playbook that owns the request: the Commands table's reference for an explicit or clearly implied sub-command, or [reference/new-work.md](reference/new-work.md) for a new surface or replacement visual world. Then inspect the target and at least one representative source of incumbent visual truth (tokens, theme, CSS, component, or asset) before editing. 3. After analysis and direction are resolved, load [reference/craft-floor.md](reference/craft-floor.md) immediately before editing UI. It carries the quality floor, the absolute bans, and the reflexes no detector catches. Do not load it for planning-only work. diff --git a/skill/agents/impeccable-finish-reviewer.md b/skill/agents/impeccable-finish-reviewer.md index 588c1545e..ed7e82fc7 100644 --- a/skill/agents/impeccable-finish-reviewer.md +++ b/skill/agents/impeccable-finish-reviewer.md @@ -22,7 +22,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order @@ -45,4 +45,4 @@ Return the disposition line first, then exactly five sections: `persistence` (pa ## Verdict Pass -When the parent returns with post-fix recaptures, you are scoring, not re-hunting. The parent's narration of what was fixed is not evidence; a claimed fix you cannot see in the recaptures is unresolved. For each material fix from your review, one line: resolved, partial, or unresolved, tied to what the new screenshots visibly show; a fix answered mechanically, positions moved but the quality the finding named still absent, is partial at best. Then name at most three regressions the fix batch itself introduced, judged by the same matrix rules, and nothing else; no new hunt, no new checks. Return exactly two sections: `verdict` (the scored list) and `remaining` (what stays open, or "clear"), and end with the disposition line recomputed against what remains open; unresolved or partial material findings can never recompute to ship. +When the parent returns with post-fix recaptures, you are scoring, not re-hunting. The parent recaptures over the same screenshot files you read in the review round, so re-read those exact paths for this round; a round-stamped filename you invent points at nothing. The parent's narration of what was fixed is not evidence; a claimed fix you cannot see in the recaptures is unresolved. For each material fix from your review, one line: resolved, partial, or unresolved, tied to what the new screenshots visibly show; a fix answered mechanically, positions moved but the quality the finding named still absent, is partial at best. Then name at most three regressions the fix batch itself introduced, judged by the same matrix rules, and nothing else; no new hunt, no new checks. Return exactly two sections: `verdict` (the scored list) and `remaining` (what stays open, or "clear"), and end with the disposition line recomputed against what remains open; unresolved or partial material findings can never recompute to ship. diff --git a/skill/reference/new-work.md b/skill/reference/new-work.md index adddcc12e..d745fdf5d 100644 --- a/skill/reference/new-work.md +++ b/skill/reference/new-work.md @@ -109,6 +109,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node {{scripts_path}}/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node {{scripts_path}}/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/tests/build.test.js b/tests/build.test.js index 44a21dd8d..6f6865237 100644 --- a/tests/build.test.js +++ b/tests/build.test.js @@ -647,3 +647,87 @@ describe('Cursor subagent generation', () => { expect(assetProducer).toContain('.cursor/skills/impeccable/scripts'); }); }); + +// Regression guard for the gap that shipped literal `{{scripts_path}}` inside +// the Codex dists' nested agent .toml: three separate code paths emit an agent +// body, and one of them skipped placeholder substitution and rule-marker +// stripping. Assert every surface, not just the one that was broken. +describe('agent bodies resolve placeholders on every surface that ships them', () => { + const ROOT = process.cwd(); + const AGENT_TEST_DIR = path.join(ROOT, 'test-tmp-agent-placeholders'); + const DIST = path.join(AGENT_TEST_DIR, 'dist'); + + // [emitted file, the scripts path that provider installs to] + const SURFACES = [ + // Nested Codex .toml: the skill install is the whole delivery for these. + ['codex/.codex/skills/impeccable/agents/impeccable_asset_producer.toml', '.codex/skills/impeccable/scripts'], + ['agents/.agents/skills/impeccable/agents/impeccable_asset_producer.toml', '.agents/skills/impeccable/scripts'], + // Native agent files. + ['claude-code/.claude/agents/impeccable-asset-producer.md', '.claude/skills/impeccable/scripts'], + ['github/.github/agents/impeccable-asset-producer.agent.md', '.github/skills/impeccable/scripts'], + ['grok/.grok/agents/impeccable-asset-producer.md', '.grok/skills/impeccable/scripts'], + // Degraded fallback reference generated from the same agent definition. + ['codex/.codex/skills/impeccable/reference/degraded/asset-producer.md', '.codex/skills/impeccable/scripts'], + ]; + + beforeEach(() => { + if (fs.existsSync(AGENT_TEST_DIR)) fs.rmSync(AGENT_TEST_DIR, { recursive: true, force: true }); + fs.mkdirSync(AGENT_TEST_DIR, { recursive: true }); + const { skills } = utils.readSourceFiles(ROOT); + transformers.transformCodex(skills, DIST); + transformers.transformAgents(skills, DIST); + transformers.transformClaudeCode(skills, DIST); + transformers.transformGitHub(skills, DIST); + transformers.transformGrok(skills, DIST); + }); + + afterEach(() => { + if (fs.existsSync(AGENT_TEST_DIR)) fs.rmSync(AGENT_TEST_DIR, { recursive: true, force: true }); + }); + + test('the asset producer ships a runnable embed-prompt command, never the raw token', () => { + for (const [relPath, scriptsPath] of SURFACES) { + const content = fs.readFileSync(path.join(DIST, relPath), 'utf-8'); + expect(content).toContain(`node ${scriptsPath}/embed-prompt.mjs`); + expect(content).not.toContain('{{scripts_path}}'); + } + }); + + test('no emitted agent body carries an unresolved placeholder or a rule marker', () => { + const synthetic = { + name: 'impeccable', + description: 'synthetic', + body: 'Synthetic skill body.', + agents: [ + { + name: 'impeccable-synthetic', + codexName: 'impeccable_synthetic', + description: 'synthetic agent', + body: 'Run `node {{scripts_path}}/embed-prompt.mjs` and ask {{model}}. ', + }, + ], + }; + const synthDist = path.join(AGENT_TEST_DIR, 'synth'); + transformers.transformCodex([synthetic], synthDist); + transformers.transformClaudeCode([synthetic], synthDist); + + const emitted = [ + 'codex/.codex/skills/impeccable/agents/impeccable_synthetic.toml', + 'codex/.codex/skills/impeccable/reference/degraded/synthetic.md', + 'claude-code/.claude/agents/impeccable-synthetic.md', + ]; + for (const relPath of emitted) { + const content = fs.readFileSync(path.join(synthDist, relPath), 'utf-8'); + expect(content).not.toContain('{{'); + expect(content).not.toMatch(/ - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/skill/reference/ios.md b/skill/reference/ios.md index 9d2ba20a0..bff06b2a3 100644 --- a/skill/reference/ios.md +++ b/skill/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/skill/reference/new-work.md b/skill/reference/new-work.md index d745fdf5d..f9b785dcd 100644 --- a/skill/reference/new-work.md +++ b/skill/reference/new-work.md @@ -107,8 +107,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node {{scripts_path}}/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node {{scripts_path}}/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. From c38ad8fb8b63aea7eccd316a919f62ea9142f3ab Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 9 Aug 2026 22:10:23 +0000 Subject: [PATCH 16/29] Sync generated provider output --- .agents/skills/impeccable/SKILL.md | 2 +- .../impeccable/agents/impeccable_finish_reviewer.toml | 2 +- .agents/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .agents/skills/impeccable/reference/ios.md | 6 ++++++ .agents/skills/impeccable/reference/new-work.md | 4 ++-- .claude/agents/impeccable-finish-reviewer.md | 2 +- .claude/skills/impeccable/SKILL.md | 2 +- .claude/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .claude/skills/impeccable/reference/ios.md | 6 ++++++ .claude/skills/impeccable/reference/new-work.md | 4 ++-- .cursor/agents/impeccable-finish-reviewer.md | 2 +- .cursor/skills/impeccable/SKILL.md | 2 +- .cursor/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .cursor/skills/impeccable/reference/ios.md | 6 ++++++ .cursor/skills/impeccable/reference/new-work.md | 4 ++-- .gemini/skills/impeccable/SKILL.md | 2 +- .gemini/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .gemini/skills/impeccable/reference/ios.md | 6 ++++++ .gemini/skills/impeccable/reference/new-work.md | 4 ++-- .github/skills/impeccable/SKILL.md | 2 +- .github/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .github/skills/impeccable/reference/ios.md | 6 ++++++ .github/skills/impeccable/reference/new-work.md | 4 ++-- .grok/agents/impeccable-finish-reviewer.md | 2 +- .grok/skills/impeccable/SKILL.md | 2 +- .grok/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .grok/skills/impeccable/reference/ios.md | 6 ++++++ .grok/skills/impeccable/reference/new-work.md | 4 ++-- .kiro/skills/impeccable/SKILL.md | 2 +- .kiro/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .kiro/skills/impeccable/reference/ios.md | 6 ++++++ .kiro/skills/impeccable/reference/new-work.md | 4 ++-- .opencode/skills/impeccable/SKILL.md | 2 +- .opencode/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .opencode/skills/impeccable/reference/ios.md | 6 ++++++ .opencode/skills/impeccable/reference/new-work.md | 4 ++-- .pi/skills/impeccable/SKILL.md | 2 +- .pi/skills/impeccable/reference/android.md | 6 ++++++ .pi/skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .pi/skills/impeccable/reference/ios.md | 6 ++++++ .pi/skills/impeccable/reference/new-work.md | 4 ++-- .qoder/skills/impeccable/SKILL.md | 2 +- .qoder/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .qoder/skills/impeccable/reference/ios.md | 6 ++++++ .qoder/skills/impeccable/reference/new-work.md | 4 ++-- .rovodev/skills/impeccable/SKILL.md | 2 +- .rovodev/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .rovodev/skills/impeccable/reference/ios.md | 6 ++++++ .rovodev/skills/impeccable/reference/new-work.md | 4 ++-- .trae-cn/skills/impeccable/SKILL.md | 2 +- .trae-cn/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .trae-cn/skills/impeccable/reference/ios.md | 6 ++++++ .trae-cn/skills/impeccable/reference/new-work.md | 4 ++-- .trae/skills/impeccable/SKILL.md | 2 +- .trae/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .trae/skills/impeccable/reference/ios.md | 6 ++++++ .trae/skills/impeccable/reference/new-work.md | 4 ++-- .vibe/skills/impeccable/SKILL.md | 2 +- .vibe/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .vibe/skills/impeccable/reference/ios.md | 6 ++++++ .vibe/skills/impeccable/reference/new-work.md | 4 ++-- plugin/agents/impeccable-finish-reviewer.md | 2 +- plugin/skills/impeccable/SKILL.md | 2 +- plugin/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- plugin/skills/impeccable/reference/ios.md | 6 ++++++ plugin/skills/impeccable/reference/new-work.md | 4 ++-- 80 files changed, 245 insertions(+), 65 deletions(-) diff --git a/.agents/skills/impeccable/SKILL.md b/.agents/skills/impeccable/SKILL.md index 347501caf..72f07c311 100644 --- a/.agents/skills/impeccable/SKILL.md +++ b/.agents/skills/impeccable/SKILL.md @@ -9,7 +9,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.agents/skills/impeccable/agents/impeccable_finish_reviewer.toml b/.agents/skills/impeccable/agents/impeccable_finish_reviewer.toml index 833b511aa..91b45993d 100644 --- a/.agents/skills/impeccable/agents/impeccable_finish_reviewer.toml +++ b/.agents/skills/impeccable/agents/impeccable_finish_reviewer.toml @@ -13,7 +13,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.agents/skills/impeccable/reference/android.md b/.agents/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.agents/skills/impeccable/reference/android.md +++ b/.agents/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.agents/skills/impeccable/reference/degraded/finish-reviewer.md b/.agents/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.agents/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.agents/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.agents/skills/impeccable/reference/ios.md b/.agents/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.agents/skills/impeccable/reference/ios.md +++ b/.agents/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.agents/skills/impeccable/reference/new-work.md b/.agents/skills/impeccable/reference/new-work.md index ab8059c90..ca489abf8 100644 --- a/.agents/skills/impeccable/reference/new-work.md +++ b/.agents/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .agents/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .agents/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.claude/agents/impeccable-finish-reviewer.md b/.claude/agents/impeccable-finish-reviewer.md index 76f830597..d03529403 100644 --- a/.claude/agents/impeccable-finish-reviewer.md +++ b/.claude/agents/impeccable-finish-reviewer.md @@ -16,7 +16,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.claude/skills/impeccable/SKILL.md b/.claude/skills/impeccable/SKILL.md index ee8937bc4..43920ae2b 100644 --- a/.claude/skills/impeccable/SKILL.md +++ b/.claude/skills/impeccable/SKILL.md @@ -15,7 +15,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.claude/skills/impeccable/reference/android.md b/.claude/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.claude/skills/impeccable/reference/android.md +++ b/.claude/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.claude/skills/impeccable/reference/degraded/finish-reviewer.md b/.claude/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.claude/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.claude/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.claude/skills/impeccable/reference/ios.md b/.claude/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.claude/skills/impeccable/reference/ios.md +++ b/.claude/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.claude/skills/impeccable/reference/new-work.md b/.claude/skills/impeccable/reference/new-work.md index 9a70a0b8e..859b84a6b 100644 --- a/.claude/skills/impeccable/reference/new-work.md +++ b/.claude/skills/impeccable/reference/new-work.md @@ -105,8 +105,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .claude/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .claude/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.cursor/agents/impeccable-finish-reviewer.md b/.cursor/agents/impeccable-finish-reviewer.md index 1cdf3f3a9..544a1e464 100644 --- a/.cursor/agents/impeccable-finish-reviewer.md +++ b/.cursor/agents/impeccable-finish-reviewer.md @@ -15,7 +15,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.cursor/skills/impeccable/SKILL.md b/.cursor/skills/impeccable/SKILL.md index c4af4ee53..7584a45ff 100644 --- a/.cursor/skills/impeccable/SKILL.md +++ b/.cursor/skills/impeccable/SKILL.md @@ -10,7 +10,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.cursor/skills/impeccable/reference/android.md b/.cursor/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.cursor/skills/impeccable/reference/android.md +++ b/.cursor/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.cursor/skills/impeccable/reference/degraded/finish-reviewer.md b/.cursor/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.cursor/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.cursor/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.cursor/skills/impeccable/reference/ios.md b/.cursor/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.cursor/skills/impeccable/reference/ios.md +++ b/.cursor/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.cursor/skills/impeccable/reference/new-work.md b/.cursor/skills/impeccable/reference/new-work.md index de8878bd9..e519eb9cd 100644 --- a/.cursor/skills/impeccable/reference/new-work.md +++ b/.cursor/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .cursor/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .cursor/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.gemini/skills/impeccable/SKILL.md b/.gemini/skills/impeccable/SKILL.md index ba895a5c9..62bdd7fc6 100644 --- a/.gemini/skills/impeccable/SKILL.md +++ b/.gemini/skills/impeccable/SKILL.md @@ -9,7 +9,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.gemini/skills/impeccable/reference/android.md b/.gemini/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.gemini/skills/impeccable/reference/android.md +++ b/.gemini/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.gemini/skills/impeccable/reference/degraded/finish-reviewer.md b/.gemini/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.gemini/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.gemini/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.gemini/skills/impeccable/reference/ios.md b/.gemini/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.gemini/skills/impeccable/reference/ios.md +++ b/.gemini/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.gemini/skills/impeccable/reference/new-work.md b/.gemini/skills/impeccable/reference/new-work.md index 8874c2486..93840d013 100644 --- a/.gemini/skills/impeccable/reference/new-work.md +++ b/.gemini/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .gemini/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .gemini/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.github/skills/impeccable/SKILL.md b/.github/skills/impeccable/SKILL.md index 6c45dfc99..b53ababd1 100644 --- a/.github/skills/impeccable/SKILL.md +++ b/.github/skills/impeccable/SKILL.md @@ -12,7 +12,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.github/skills/impeccable/reference/android.md b/.github/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.github/skills/impeccable/reference/android.md +++ b/.github/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.github/skills/impeccable/reference/degraded/finish-reviewer.md b/.github/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.github/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.github/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.github/skills/impeccable/reference/ios.md b/.github/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.github/skills/impeccable/reference/ios.md +++ b/.github/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.github/skills/impeccable/reference/new-work.md b/.github/skills/impeccable/reference/new-work.md index 75d8133ad..e39008588 100644 --- a/.github/skills/impeccable/reference/new-work.md +++ b/.github/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .github/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .github/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.grok/agents/impeccable-finish-reviewer.md b/.grok/agents/impeccable-finish-reviewer.md index 76f830597..d03529403 100644 --- a/.grok/agents/impeccable-finish-reviewer.md +++ b/.grok/agents/impeccable-finish-reviewer.md @@ -16,7 +16,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.grok/skills/impeccable/SKILL.md b/.grok/skills/impeccable/SKILL.md index 09a1a4797..d0f9c8142 100644 --- a/.grok/skills/impeccable/SKILL.md +++ b/.grok/skills/impeccable/SKILL.md @@ -15,7 +15,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.grok/skills/impeccable/reference/android.md b/.grok/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.grok/skills/impeccable/reference/android.md +++ b/.grok/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.grok/skills/impeccable/reference/degraded/finish-reviewer.md b/.grok/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.grok/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.grok/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.grok/skills/impeccable/reference/ios.md b/.grok/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.grok/skills/impeccable/reference/ios.md +++ b/.grok/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.grok/skills/impeccable/reference/new-work.md b/.grok/skills/impeccable/reference/new-work.md index f3aacee42..884db3fd0 100644 --- a/.grok/skills/impeccable/reference/new-work.md +++ b/.grok/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .grok/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .grok/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.kiro/skills/impeccable/SKILL.md b/.kiro/skills/impeccable/SKILL.md index bdbd7bf63..0b23316a7 100644 --- a/.kiro/skills/impeccable/SKILL.md +++ b/.kiro/skills/impeccable/SKILL.md @@ -10,7 +10,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.kiro/skills/impeccable/reference/android.md b/.kiro/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.kiro/skills/impeccable/reference/android.md +++ b/.kiro/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.kiro/skills/impeccable/reference/degraded/finish-reviewer.md b/.kiro/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.kiro/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.kiro/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.kiro/skills/impeccable/reference/ios.md b/.kiro/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.kiro/skills/impeccable/reference/ios.md +++ b/.kiro/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.kiro/skills/impeccable/reference/new-work.md b/.kiro/skills/impeccable/reference/new-work.md index d5d6b99b6..0b7bd950b 100644 --- a/.kiro/skills/impeccable/reference/new-work.md +++ b/.kiro/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .kiro/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .kiro/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.opencode/skills/impeccable/SKILL.md b/.opencode/skills/impeccable/SKILL.md index f111787da..5c20ef199 100644 --- a/.opencode/skills/impeccable/SKILL.md +++ b/.opencode/skills/impeccable/SKILL.md @@ -15,7 +15,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.opencode/skills/impeccable/reference/android.md b/.opencode/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.opencode/skills/impeccable/reference/android.md +++ b/.opencode/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.opencode/skills/impeccable/reference/degraded/finish-reviewer.md b/.opencode/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.opencode/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.opencode/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.opencode/skills/impeccable/reference/ios.md b/.opencode/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.opencode/skills/impeccable/reference/ios.md +++ b/.opencode/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.opencode/skills/impeccable/reference/new-work.md b/.opencode/skills/impeccable/reference/new-work.md index 5848ad74b..09792494c 100644 --- a/.opencode/skills/impeccable/reference/new-work.md +++ b/.opencode/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .opencode/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .opencode/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.pi/skills/impeccable/SKILL.md b/.pi/skills/impeccable/SKILL.md index 2916526aa..460996938 100644 --- a/.pi/skills/impeccable/SKILL.md +++ b/.pi/skills/impeccable/SKILL.md @@ -13,7 +13,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.pi/skills/impeccable/reference/android.md b/.pi/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.pi/skills/impeccable/reference/android.md +++ b/.pi/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.pi/skills/impeccable/reference/degraded/finish-reviewer.md b/.pi/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.pi/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.pi/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.pi/skills/impeccable/reference/ios.md b/.pi/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.pi/skills/impeccable/reference/ios.md +++ b/.pi/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.pi/skills/impeccable/reference/new-work.md b/.pi/skills/impeccable/reference/new-work.md index 78e86c1bb..396dee5fc 100644 --- a/.pi/skills/impeccable/reference/new-work.md +++ b/.pi/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .pi/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .pi/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.qoder/skills/impeccable/SKILL.md b/.qoder/skills/impeccable/SKILL.md index a418eb317..cbef14a60 100644 --- a/.qoder/skills/impeccable/SKILL.md +++ b/.qoder/skills/impeccable/SKILL.md @@ -15,7 +15,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.qoder/skills/impeccable/reference/android.md b/.qoder/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.qoder/skills/impeccable/reference/android.md +++ b/.qoder/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.qoder/skills/impeccable/reference/degraded/finish-reviewer.md b/.qoder/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.qoder/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.qoder/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.qoder/skills/impeccable/reference/ios.md b/.qoder/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.qoder/skills/impeccable/reference/ios.md +++ b/.qoder/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.qoder/skills/impeccable/reference/new-work.md b/.qoder/skills/impeccable/reference/new-work.md index 71ab4796e..286b9ad45 100644 --- a/.qoder/skills/impeccable/reference/new-work.md +++ b/.qoder/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .qoder/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .qoder/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.rovodev/skills/impeccable/SKILL.md b/.rovodev/skills/impeccable/SKILL.md index c1d0806fa..648660e93 100644 --- a/.rovodev/skills/impeccable/SKILL.md +++ b/.rovodev/skills/impeccable/SKILL.md @@ -15,7 +15,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.rovodev/skills/impeccable/reference/android.md b/.rovodev/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.rovodev/skills/impeccable/reference/android.md +++ b/.rovodev/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.rovodev/skills/impeccable/reference/degraded/finish-reviewer.md b/.rovodev/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.rovodev/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.rovodev/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.rovodev/skills/impeccable/reference/ios.md b/.rovodev/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.rovodev/skills/impeccable/reference/ios.md +++ b/.rovodev/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.rovodev/skills/impeccable/reference/new-work.md b/.rovodev/skills/impeccable/reference/new-work.md index 29218d01f..47f451c6b 100644 --- a/.rovodev/skills/impeccable/reference/new-work.md +++ b/.rovodev/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .rovodev/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .rovodev/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.trae-cn/skills/impeccable/SKILL.md b/.trae-cn/skills/impeccable/SKILL.md index b622286b3..84992102d 100644 --- a/.trae-cn/skills/impeccable/SKILL.md +++ b/.trae-cn/skills/impeccable/SKILL.md @@ -12,7 +12,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.trae-cn/skills/impeccable/reference/android.md b/.trae-cn/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.trae-cn/skills/impeccable/reference/android.md +++ b/.trae-cn/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.trae-cn/skills/impeccable/reference/degraded/finish-reviewer.md b/.trae-cn/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.trae-cn/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.trae-cn/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.trae-cn/skills/impeccable/reference/ios.md b/.trae-cn/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.trae-cn/skills/impeccable/reference/ios.md +++ b/.trae-cn/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.trae-cn/skills/impeccable/reference/new-work.md b/.trae-cn/skills/impeccable/reference/new-work.md index 96faf817a..1bd18cc6c 100644 --- a/.trae-cn/skills/impeccable/reference/new-work.md +++ b/.trae-cn/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .trae-cn/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .trae-cn/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.trae/skills/impeccable/SKILL.md b/.trae/skills/impeccable/SKILL.md index 7c7640b97..805d762c8 100644 --- a/.trae/skills/impeccable/SKILL.md +++ b/.trae/skills/impeccable/SKILL.md @@ -12,7 +12,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.trae/skills/impeccable/reference/android.md b/.trae/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.trae/skills/impeccable/reference/android.md +++ b/.trae/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.trae/skills/impeccable/reference/degraded/finish-reviewer.md b/.trae/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.trae/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.trae/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.trae/skills/impeccable/reference/ios.md b/.trae/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.trae/skills/impeccable/reference/ios.md +++ b/.trae/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.trae/skills/impeccable/reference/new-work.md b/.trae/skills/impeccable/reference/new-work.md index c60a1247b..aa6defef0 100644 --- a/.trae/skills/impeccable/reference/new-work.md +++ b/.trae/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .trae/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .trae/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.vibe/skills/impeccable/SKILL.md b/.vibe/skills/impeccable/SKILL.md index d3a201210..780ccdec0 100644 --- a/.vibe/skills/impeccable/SKILL.md +++ b/.vibe/skills/impeccable/SKILL.md @@ -14,7 +14,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.vibe/skills/impeccable/reference/android.md b/.vibe/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.vibe/skills/impeccable/reference/android.md +++ b/.vibe/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.vibe/skills/impeccable/reference/degraded/finish-reviewer.md b/.vibe/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.vibe/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.vibe/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.vibe/skills/impeccable/reference/ios.md b/.vibe/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.vibe/skills/impeccable/reference/ios.md +++ b/.vibe/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.vibe/skills/impeccable/reference/new-work.md b/.vibe/skills/impeccable/reference/new-work.md index b426805a4..6d625bd64 100644 --- a/.vibe/skills/impeccable/reference/new-work.md +++ b/.vibe/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .vibe/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .vibe/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/plugin/agents/impeccable-finish-reviewer.md b/plugin/agents/impeccable-finish-reviewer.md index 76f830597..d03529403 100644 --- a/plugin/agents/impeccable-finish-reviewer.md +++ b/plugin/agents/impeccable-finish-reviewer.md @@ -16,7 +16,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/plugin/skills/impeccable/SKILL.md b/plugin/skills/impeccable/SKILL.md index ee8937bc4..43920ae2b 100644 --- a/plugin/skills/impeccable/SKILL.md +++ b/plugin/skills/impeccable/SKILL.md @@ -15,7 +15,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/plugin/skills/impeccable/reference/android.md b/plugin/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/plugin/skills/impeccable/reference/android.md +++ b/plugin/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/plugin/skills/impeccable/reference/degraded/finish-reviewer.md b/plugin/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/plugin/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/plugin/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/plugin/skills/impeccable/reference/ios.md b/plugin/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/plugin/skills/impeccable/reference/ios.md +++ b/plugin/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/plugin/skills/impeccable/reference/new-work.md b/plugin/skills/impeccable/reference/new-work.md index 9a70a0b8e..859b84a6b 100644 --- a/plugin/skills/impeccable/reference/new-work.md +++ b/plugin/skills/impeccable/reference/new-work.md @@ -105,8 +105,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .claude/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .claude/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. From 181212cb2dce0f9038acd99a1a9d19f040a19fba Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Sun, 9 Aug 2026 15:35:07 -0700 Subject: [PATCH 17/29] Map polish's evidence and verify steps per platform (#550) * Map polish's evidence and verify steps per platform polish.md was the last command reference verifying through web-only vocabulary after #546 gave the pipeline its native leg. Three targeted mappings, following the in-file precedent new-work.md set (the classify-triage-polish-verify flow itself is platform-neutral, so no polish.native.md): - Evidence gathering: desktop and mobile sizes on the web; the shipped device classes on simulator, emulator, or hardware on native, per the platform reference's Verifying the build section. - Verify checklist layouts: phone and tablet size classes, both orientations where supported, on native. - Verify checklist "supported browsers": native has none, so the analogues are named (runtime warnings, dropped frames, supported OS versions). Assisted-by: Claude Code * fix: branch the verify checklist web-vs-native explicitly Copilot follow-up: the parenthetical style could read as both term sets applying on native. The two bullets now branch explicitly, and the shared items (console errors, layout shift, latency, image loading) stay unbranched since they apply everywhere. Assisted-by: Claude Code * fix: restore runtime warnings to the native verify branch greptile follow-up: the explicit-branch restyle dropped the runtime warnings requirement the parenthetical carried; folding it into "console errors everywhere" hid it behind web vocabulary. It is back as its own item in the native branch. Assisted-by: Claude Code --- skill/reference/polish.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skill/reference/polish.md b/skill/reference/polish.md index d58ca3a49..1d2bfcff3 100644 --- a/skill/reference/polish.md +++ b/skill/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. From 29e5b1494c0e8f8eaa583ad10657f8564031313c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 9 Aug 2026 22:35:43 +0000 Subject: [PATCH 18/29] Sync generated provider output --- .agents/skills/impeccable/reference/polish.md | 6 +++--- .claude/skills/impeccable/reference/polish.md | 6 +++--- .cursor/skills/impeccable/reference/polish.md | 6 +++--- .gemini/skills/impeccable/reference/polish.md | 6 +++--- .github/skills/impeccable/reference/polish.md | 6 +++--- .grok/skills/impeccable/reference/polish.md | 6 +++--- .kiro/skills/impeccable/reference/polish.md | 6 +++--- .opencode/skills/impeccable/reference/polish.md | 6 +++--- .pi/skills/impeccable/reference/polish.md | 6 +++--- .qoder/skills/impeccable/reference/polish.md | 6 +++--- .rovodev/skills/impeccable/reference/polish.md | 6 +++--- .trae-cn/skills/impeccable/reference/polish.md | 6 +++--- .trae/skills/impeccable/reference/polish.md | 6 +++--- .vibe/skills/impeccable/reference/polish.md | 6 +++--- plugin/skills/impeccable/reference/polish.md | 6 +++--- 15 files changed, 45 insertions(+), 45 deletions(-) diff --git a/.agents/skills/impeccable/reference/polish.md b/.agents/skills/impeccable/reference/polish.md index b18014fe5..a9fd13935 100644 --- a/.agents/skills/impeccable/reference/polish.md +++ b/.agents/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.claude/skills/impeccable/reference/polish.md b/.claude/skills/impeccable/reference/polish.md index a23c3887f..9db878697 100644 --- a/.claude/skills/impeccable/reference/polish.md +++ b/.claude/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.cursor/skills/impeccable/reference/polish.md b/.cursor/skills/impeccable/reference/polish.md index f7ad2f44b..7af88f60c 100644 --- a/.cursor/skills/impeccable/reference/polish.md +++ b/.cursor/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.gemini/skills/impeccable/reference/polish.md b/.gemini/skills/impeccable/reference/polish.md index f3cfc9c39..877a83c84 100644 --- a/.gemini/skills/impeccable/reference/polish.md +++ b/.gemini/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.github/skills/impeccable/reference/polish.md b/.github/skills/impeccable/reference/polish.md index 7bdf7eb5c..0e115f981 100644 --- a/.github/skills/impeccable/reference/polish.md +++ b/.github/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.grok/skills/impeccable/reference/polish.md b/.grok/skills/impeccable/reference/polish.md index c9c14dd4d..e5ff9c4b5 100644 --- a/.grok/skills/impeccable/reference/polish.md +++ b/.grok/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.kiro/skills/impeccable/reference/polish.md b/.kiro/skills/impeccable/reference/polish.md index 391142469..fee049ce1 100644 --- a/.kiro/skills/impeccable/reference/polish.md +++ b/.kiro/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.opencode/skills/impeccable/reference/polish.md b/.opencode/skills/impeccable/reference/polish.md index 6e4709750..d46ea12c9 100644 --- a/.opencode/skills/impeccable/reference/polish.md +++ b/.opencode/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.pi/skills/impeccable/reference/polish.md b/.pi/skills/impeccable/reference/polish.md index fdb9ef52d..fb26c7fca 100644 --- a/.pi/skills/impeccable/reference/polish.md +++ b/.pi/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.qoder/skills/impeccable/reference/polish.md b/.qoder/skills/impeccable/reference/polish.md index ae688924a..b392fe8fd 100644 --- a/.qoder/skills/impeccable/reference/polish.md +++ b/.qoder/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.rovodev/skills/impeccable/reference/polish.md b/.rovodev/skills/impeccable/reference/polish.md index 21f624ece..b18f133ad 100644 --- a/.rovodev/skills/impeccable/reference/polish.md +++ b/.rovodev/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.trae-cn/skills/impeccable/reference/polish.md b/.trae-cn/skills/impeccable/reference/polish.md index 5bbe2215d..56789b001 100644 --- a/.trae-cn/skills/impeccable/reference/polish.md +++ b/.trae-cn/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.trae/skills/impeccable/reference/polish.md b/.trae/skills/impeccable/reference/polish.md index 2f5cdff3f..ffd4ea340 100644 --- a/.trae/skills/impeccable/reference/polish.md +++ b/.trae/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.vibe/skills/impeccable/reference/polish.md b/.vibe/skills/impeccable/reference/polish.md index bad65d956..7e7d52c3f 100644 --- a/.vibe/skills/impeccable/reference/polish.md +++ b/.vibe/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/plugin/skills/impeccable/reference/polish.md b/plugin/skills/impeccable/reference/polish.md index a23c3887f..9db878697 100644 --- a/plugin/skills/impeccable/reference/polish.md +++ b/plugin/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. From ab9a29728b0247d8b0c5e5132656c76d79e3d72e Mon Sep 17 00:00:00 2001 From: Abarnaa Sree N Date: Mon, 10 Aug 2026 04:18:07 +0530 Subject: [PATCH 19/29] Warn when static HTML parser dependencies are unavailable (#465) --- cli/engine/engines/static-html/detect-html.mjs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/cli/engine/engines/static-html/detect-html.mjs b/cli/engine/engines/static-html/detect-html.mjs index 482ba0cc3..e08589440 100644 --- a/cli/engine/engines/static-html/detect-html.mjs +++ b/cli/engine/engines/static-html/detect-html.mjs @@ -138,10 +138,21 @@ async function detectHtml(filePath, options = {}) { domutils, }; }); - } catch { - return detectText(html, filePath, options); + } catch (err) { + if (!globalThis.__impeccableStaticHtmlWarned) { + globalThis.__impeccableStaticHtmlWarned = true; + + process.stderr.write( + 'impeccable detect: DEGRADED - HTML parser modules unavailable ' + + '(htmlparser2, css-select, css-tree, domutils).\n' + + 'Falling back to regex matching. Custom properties, selector matching and computed ' + + 'contrast are NOT evaluated; findings are an undercount, not a clean bill of health.\n' +); } + return detectText(html, filePath, options); +} + const resolvedPath = path.resolve(filePath); const fileDir = path.dirname(resolvedPath); const root = profileStep(profile, { From 0dd4f90af64210c1ebad200dd36b4ef1618fa875 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 9 Aug 2026 22:48:39 +0000 Subject: [PATCH 20/29] Sync generated provider output --- .../detector/engines/static-html/detect-html.mjs | 15 +++++++++++++-- .../detector/engines/static-html/detect-html.mjs | 15 +++++++++++++-- .../detector/engines/static-html/detect-html.mjs | 15 +++++++++++++-- .../detector/engines/static-html/detect-html.mjs | 15 +++++++++++++-- .../detector/engines/static-html/detect-html.mjs | 15 +++++++++++++-- .../detector/engines/static-html/detect-html.mjs | 15 +++++++++++++-- .../detector/engines/static-html/detect-html.mjs | 15 +++++++++++++-- .../detector/engines/static-html/detect-html.mjs | 15 +++++++++++++-- .../detector/engines/static-html/detect-html.mjs | 15 +++++++++++++-- .../detector/engines/static-html/detect-html.mjs | 15 +++++++++++++-- .../detector/engines/static-html/detect-html.mjs | 15 +++++++++++++-- .../detector/engines/static-html/detect-html.mjs | 15 +++++++++++++-- .../detector/engines/static-html/detect-html.mjs | 15 +++++++++++++-- .../detector/engines/static-html/detect-html.mjs | 15 +++++++++++++-- .../detector/engines/static-html/detect-html.mjs | 15 +++++++++++++-- 15 files changed, 195 insertions(+), 30 deletions(-) diff --git a/.agents/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.agents/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index 482ba0cc3..e08589440 100644 --- a/.agents/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.agents/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -138,10 +138,21 @@ async function detectHtml(filePath, options = {}) { domutils, }; }); - } catch { - return detectText(html, filePath, options); + } catch (err) { + if (!globalThis.__impeccableStaticHtmlWarned) { + globalThis.__impeccableStaticHtmlWarned = true; + + process.stderr.write( + 'impeccable detect: DEGRADED - HTML parser modules unavailable ' + + '(htmlparser2, css-select, css-tree, domutils).\n' + + 'Falling back to regex matching. Custom properties, selector matching and computed ' + + 'contrast are NOT evaluated; findings are an undercount, not a clean bill of health.\n' +); } + return detectText(html, filePath, options); +} + const resolvedPath = path.resolve(filePath); const fileDir = path.dirname(resolvedPath); const root = profileStep(profile, { diff --git a/.claude/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.claude/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index 482ba0cc3..e08589440 100644 --- a/.claude/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.claude/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -138,10 +138,21 @@ async function detectHtml(filePath, options = {}) { domutils, }; }); - } catch { - return detectText(html, filePath, options); + } catch (err) { + if (!globalThis.__impeccableStaticHtmlWarned) { + globalThis.__impeccableStaticHtmlWarned = true; + + process.stderr.write( + 'impeccable detect: DEGRADED - HTML parser modules unavailable ' + + '(htmlparser2, css-select, css-tree, domutils).\n' + + 'Falling back to regex matching. Custom properties, selector matching and computed ' + + 'contrast are NOT evaluated; findings are an undercount, not a clean bill of health.\n' +); } + return detectText(html, filePath, options); +} + const resolvedPath = path.resolve(filePath); const fileDir = path.dirname(resolvedPath); const root = profileStep(profile, { diff --git a/.cursor/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.cursor/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index 482ba0cc3..e08589440 100644 --- a/.cursor/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.cursor/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -138,10 +138,21 @@ async function detectHtml(filePath, options = {}) { domutils, }; }); - } catch { - return detectText(html, filePath, options); + } catch (err) { + if (!globalThis.__impeccableStaticHtmlWarned) { + globalThis.__impeccableStaticHtmlWarned = true; + + process.stderr.write( + 'impeccable detect: DEGRADED - HTML parser modules unavailable ' + + '(htmlparser2, css-select, css-tree, domutils).\n' + + 'Falling back to regex matching. Custom properties, selector matching and computed ' + + 'contrast are NOT evaluated; findings are an undercount, not a clean bill of health.\n' +); } + return detectText(html, filePath, options); +} + const resolvedPath = path.resolve(filePath); const fileDir = path.dirname(resolvedPath); const root = profileStep(profile, { diff --git a/.gemini/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.gemini/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index 482ba0cc3..e08589440 100644 --- a/.gemini/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.gemini/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -138,10 +138,21 @@ async function detectHtml(filePath, options = {}) { domutils, }; }); - } catch { - return detectText(html, filePath, options); + } catch (err) { + if (!globalThis.__impeccableStaticHtmlWarned) { + globalThis.__impeccableStaticHtmlWarned = true; + + process.stderr.write( + 'impeccable detect: DEGRADED - HTML parser modules unavailable ' + + '(htmlparser2, css-select, css-tree, domutils).\n' + + 'Falling back to regex matching. Custom properties, selector matching and computed ' + + 'contrast are NOT evaluated; findings are an undercount, not a clean bill of health.\n' +); } + return detectText(html, filePath, options); +} + const resolvedPath = path.resolve(filePath); const fileDir = path.dirname(resolvedPath); const root = profileStep(profile, { diff --git a/.github/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.github/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index 482ba0cc3..e08589440 100644 --- a/.github/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.github/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -138,10 +138,21 @@ async function detectHtml(filePath, options = {}) { domutils, }; }); - } catch { - return detectText(html, filePath, options); + } catch (err) { + if (!globalThis.__impeccableStaticHtmlWarned) { + globalThis.__impeccableStaticHtmlWarned = true; + + process.stderr.write( + 'impeccable detect: DEGRADED - HTML parser modules unavailable ' + + '(htmlparser2, css-select, css-tree, domutils).\n' + + 'Falling back to regex matching. Custom properties, selector matching and computed ' + + 'contrast are NOT evaluated; findings are an undercount, not a clean bill of health.\n' +); } + return detectText(html, filePath, options); +} + const resolvedPath = path.resolve(filePath); const fileDir = path.dirname(resolvedPath); const root = profileStep(profile, { diff --git a/.grok/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.grok/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index 482ba0cc3..e08589440 100644 --- a/.grok/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.grok/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -138,10 +138,21 @@ async function detectHtml(filePath, options = {}) { domutils, }; }); - } catch { - return detectText(html, filePath, options); + } catch (err) { + if (!globalThis.__impeccableStaticHtmlWarned) { + globalThis.__impeccableStaticHtmlWarned = true; + + process.stderr.write( + 'impeccable detect: DEGRADED - HTML parser modules unavailable ' + + '(htmlparser2, css-select, css-tree, domutils).\n' + + 'Falling back to regex matching. Custom properties, selector matching and computed ' + + 'contrast are NOT evaluated; findings are an undercount, not a clean bill of health.\n' +); } + return detectText(html, filePath, options); +} + const resolvedPath = path.resolve(filePath); const fileDir = path.dirname(resolvedPath); const root = profileStep(profile, { diff --git a/.kiro/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.kiro/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index 482ba0cc3..e08589440 100644 --- a/.kiro/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.kiro/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -138,10 +138,21 @@ async function detectHtml(filePath, options = {}) { domutils, }; }); - } catch { - return detectText(html, filePath, options); + } catch (err) { + if (!globalThis.__impeccableStaticHtmlWarned) { + globalThis.__impeccableStaticHtmlWarned = true; + + process.stderr.write( + 'impeccable detect: DEGRADED - HTML parser modules unavailable ' + + '(htmlparser2, css-select, css-tree, domutils).\n' + + 'Falling back to regex matching. Custom properties, selector matching and computed ' + + 'contrast are NOT evaluated; findings are an undercount, not a clean bill of health.\n' +); } + return detectText(html, filePath, options); +} + const resolvedPath = path.resolve(filePath); const fileDir = path.dirname(resolvedPath); const root = profileStep(profile, { diff --git a/.opencode/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.opencode/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index 482ba0cc3..e08589440 100644 --- a/.opencode/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.opencode/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -138,10 +138,21 @@ async function detectHtml(filePath, options = {}) { domutils, }; }); - } catch { - return detectText(html, filePath, options); + } catch (err) { + if (!globalThis.__impeccableStaticHtmlWarned) { + globalThis.__impeccableStaticHtmlWarned = true; + + process.stderr.write( + 'impeccable detect: DEGRADED - HTML parser modules unavailable ' + + '(htmlparser2, css-select, css-tree, domutils).\n' + + 'Falling back to regex matching. Custom properties, selector matching and computed ' + + 'contrast are NOT evaluated; findings are an undercount, not a clean bill of health.\n' +); } + return detectText(html, filePath, options); +} + const resolvedPath = path.resolve(filePath); const fileDir = path.dirname(resolvedPath); const root = profileStep(profile, { diff --git a/.pi/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.pi/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index 482ba0cc3..e08589440 100644 --- a/.pi/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.pi/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -138,10 +138,21 @@ async function detectHtml(filePath, options = {}) { domutils, }; }); - } catch { - return detectText(html, filePath, options); + } catch (err) { + if (!globalThis.__impeccableStaticHtmlWarned) { + globalThis.__impeccableStaticHtmlWarned = true; + + process.stderr.write( + 'impeccable detect: DEGRADED - HTML parser modules unavailable ' + + '(htmlparser2, css-select, css-tree, domutils).\n' + + 'Falling back to regex matching. Custom properties, selector matching and computed ' + + 'contrast are NOT evaluated; findings are an undercount, not a clean bill of health.\n' +); } + return detectText(html, filePath, options); +} + const resolvedPath = path.resolve(filePath); const fileDir = path.dirname(resolvedPath); const root = profileStep(profile, { diff --git a/.qoder/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.qoder/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index 482ba0cc3..e08589440 100644 --- a/.qoder/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.qoder/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -138,10 +138,21 @@ async function detectHtml(filePath, options = {}) { domutils, }; }); - } catch { - return detectText(html, filePath, options); + } catch (err) { + if (!globalThis.__impeccableStaticHtmlWarned) { + globalThis.__impeccableStaticHtmlWarned = true; + + process.stderr.write( + 'impeccable detect: DEGRADED - HTML parser modules unavailable ' + + '(htmlparser2, css-select, css-tree, domutils).\n' + + 'Falling back to regex matching. Custom properties, selector matching and computed ' + + 'contrast are NOT evaluated; findings are an undercount, not a clean bill of health.\n' +); } + return detectText(html, filePath, options); +} + const resolvedPath = path.resolve(filePath); const fileDir = path.dirname(resolvedPath); const root = profileStep(profile, { diff --git a/.rovodev/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.rovodev/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index 482ba0cc3..e08589440 100644 --- a/.rovodev/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.rovodev/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -138,10 +138,21 @@ async function detectHtml(filePath, options = {}) { domutils, }; }); - } catch { - return detectText(html, filePath, options); + } catch (err) { + if (!globalThis.__impeccableStaticHtmlWarned) { + globalThis.__impeccableStaticHtmlWarned = true; + + process.stderr.write( + 'impeccable detect: DEGRADED - HTML parser modules unavailable ' + + '(htmlparser2, css-select, css-tree, domutils).\n' + + 'Falling back to regex matching. Custom properties, selector matching and computed ' + + 'contrast are NOT evaluated; findings are an undercount, not a clean bill of health.\n' +); } + return detectText(html, filePath, options); +} + const resolvedPath = path.resolve(filePath); const fileDir = path.dirname(resolvedPath); const root = profileStep(profile, { diff --git a/.trae-cn/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.trae-cn/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index 482ba0cc3..e08589440 100644 --- a/.trae-cn/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.trae-cn/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -138,10 +138,21 @@ async function detectHtml(filePath, options = {}) { domutils, }; }); - } catch { - return detectText(html, filePath, options); + } catch (err) { + if (!globalThis.__impeccableStaticHtmlWarned) { + globalThis.__impeccableStaticHtmlWarned = true; + + process.stderr.write( + 'impeccable detect: DEGRADED - HTML parser modules unavailable ' + + '(htmlparser2, css-select, css-tree, domutils).\n' + + 'Falling back to regex matching. Custom properties, selector matching and computed ' + + 'contrast are NOT evaluated; findings are an undercount, not a clean bill of health.\n' +); } + return detectText(html, filePath, options); +} + const resolvedPath = path.resolve(filePath); const fileDir = path.dirname(resolvedPath); const root = profileStep(profile, { diff --git a/.trae/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.trae/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index 482ba0cc3..e08589440 100644 --- a/.trae/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.trae/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -138,10 +138,21 @@ async function detectHtml(filePath, options = {}) { domutils, }; }); - } catch { - return detectText(html, filePath, options); + } catch (err) { + if (!globalThis.__impeccableStaticHtmlWarned) { + globalThis.__impeccableStaticHtmlWarned = true; + + process.stderr.write( + 'impeccable detect: DEGRADED - HTML parser modules unavailable ' + + '(htmlparser2, css-select, css-tree, domutils).\n' + + 'Falling back to regex matching. Custom properties, selector matching and computed ' + + 'contrast are NOT evaluated; findings are an undercount, not a clean bill of health.\n' +); } + return detectText(html, filePath, options); +} + const resolvedPath = path.resolve(filePath); const fileDir = path.dirname(resolvedPath); const root = profileStep(profile, { diff --git a/.vibe/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.vibe/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index 482ba0cc3..e08589440 100644 --- a/.vibe/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.vibe/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -138,10 +138,21 @@ async function detectHtml(filePath, options = {}) { domutils, }; }); - } catch { - return detectText(html, filePath, options); + } catch (err) { + if (!globalThis.__impeccableStaticHtmlWarned) { + globalThis.__impeccableStaticHtmlWarned = true; + + process.stderr.write( + 'impeccable detect: DEGRADED - HTML parser modules unavailable ' + + '(htmlparser2, css-select, css-tree, domutils).\n' + + 'Falling back to regex matching. Custom properties, selector matching and computed ' + + 'contrast are NOT evaluated; findings are an undercount, not a clean bill of health.\n' +); } + return detectText(html, filePath, options); +} + const resolvedPath = path.resolve(filePath); const fileDir = path.dirname(resolvedPath); const root = profileStep(profile, { diff --git a/plugin/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/plugin/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index 482ba0cc3..e08589440 100644 --- a/plugin/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/plugin/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -138,10 +138,21 @@ async function detectHtml(filePath, options = {}) { domutils, }; }); - } catch { - return detectText(html, filePath, options); + } catch (err) { + if (!globalThis.__impeccableStaticHtmlWarned) { + globalThis.__impeccableStaticHtmlWarned = true; + + process.stderr.write( + 'impeccable detect: DEGRADED - HTML parser modules unavailable ' + + '(htmlparser2, css-select, css-tree, domutils).\n' + + 'Falling back to regex matching. Custom properties, selector matching and computed ' + + 'contrast are NOT evaluated; findings are an undercount, not a clean bill of health.\n' +); } + return detectText(html, filePath, options); +} + const resolvedPath = path.resolve(filePath); const fileDir = path.dirname(resolvedPath); const root = profileStep(profile, { From 63fb8a56f9df4a4e8ceb0eadf241e81485a12bfe Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Sun, 9 Aug 2026 16:56:46 -0700 Subject: [PATCH 21/29] Fix Claude copy-edit prompt transport (#529) Pass staged copy-edit prompts over stdin so large batches do not exceed platform argv limits. AI assistance: Implemented and validated with OpenAI Codex under maintainer authorization. --- skill/scripts/live-copy-edit-agent.mjs | 3 +- tests/live-copy-edit-agent.test.mjs | 42 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/skill/scripts/live-copy-edit-agent.mjs b/skill/scripts/live-copy-edit-agent.mjs index 313ed7f10..f70f26ff4 100644 --- a/skill/scripts/live-copy-edit-agent.mjs +++ b/skill/scripts/live-copy-edit-agent.mjs @@ -470,12 +470,11 @@ function runClaude(prompt, { cwd, env, resultPath, logPath, timeoutMs = DEFAULT_ if (env.IMPECCABLE_LIVE_COPY_AGENT_MODEL) { args.push('--model', env.IMPECCABLE_LIVE_COPY_AGENT_MODEL); } - args.push(prompt); // Forward env as-is so CLAUDE_CODE_OAUTH_TOKEN and ANTHROPIC_API_KEY flow // through. On macOS, `claude /login` stores creds in the Keychain, which a // non-TTY subprocess cannot read; setting CLAUDE_CODE_OAUTH_TOKEN (via // `claude setup-token`) is the supported headless auth path. - return runAgentProcess('claude', args, '', { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); + return runAgentProcess('claude', args, prompt, { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); } function runAgentProcess(command, args, stdin, { cwd, env, logPath, timeoutMs, mirrorOutputPath }) { diff --git a/tests/live-copy-edit-agent.test.mjs b/tests/live-copy-edit-agent.test.mjs index 6993e5028..98d9c0c1b 100644 --- a/tests/live-copy-edit-agent.test.mjs +++ b/tests/live-copy-edit-agent.test.mjs @@ -305,6 +305,48 @@ describe('live-copy-edit-agent', () => { ); }); + it('passes large Claude prompts on stdin instead of argv', async () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'copy-agent-claude-stdin-')); + try { + const fakeClaude = path.join(tmp, 'claude'); + fs.writeFileSync(fakeClaude, [ + '#!/usr/bin/env node', + "let input = '';", + "process.stdin.setEncoding('utf8');", + "process.stdin.on('data', (chunk) => { input += chunk; });", + "process.stdin.on('end', () => {", + " const promptLeakedToArgv = process.argv.slice(2).some((arg) => arg.includes('large-prompt-sentinel'));", + " if (!input.includes('large-prompt-sentinel') || promptLeakedToArgv) process.exit(2);", + " process.stdout.write(JSON.stringify({ status: 'done', appliedEntryIds: ['large'], files: [], notes: [] }));", + '});', + '', + ].join('\n')); + fs.chmodSync(fakeClaude, 0o755); + + const result = await runCopyEditBatchAgent({ + pageUrl: '/', + entries: [{ + id: 'large', + pageUrl: '/', + ops: [{ originalText: 'Old', newText: `large-prompt-sentinel${'x'.repeat(1_100_000)}` }], + }], + }, { + provider: 'claude', + outDir: path.join(tmp, 'out'), + timeoutMs: 5_000, + env: { + ...process.env, + PATH: `${tmp}${path.delimiter}${process.env.PATH || ''}`, + }, + }); + + assert.equal(result.status, 'done'); + assert.deepEqual(result.appliedEntryIds, ['large']); + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } + }); + it('describeNoProviderError mentions starting impeccable live when chat is the missing piece', () => { const noChatPolling = describeNoProviderError({ exists: () => false, From 7fa695093eb86adc5f3c2f858df57b24466821c9 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Sun, 9 Aug 2026 16:56:49 -0700 Subject: [PATCH 22/29] Bound copy-edit prompt context (#528) * Bound copy-edit prompt context Whitelist and truncate staged operation context before it reaches the local agent prompt. AI assistance: Implemented and validated with OpenAI Codex under maintainer authorization. * Harden copy-edit prompt bounds Bound repair, candidate, and element context consistently and preserve absent source positions as null.\n\nAI assistance: Implemented and validated with OpenAI Codex under maintainer authorization. * Preserve bounded repair context Keep repair attempt metadata and nested diagnostics while retaining prompt limits.\n\nAI assistance: Implemented and validated with OpenAI Codex under maintainer authorization. --- skill/scripts/live-copy-edit-agent.mjs | 144 ++++++++++++++++++++++--- tests/live-copy-edit-agent.test.mjs | 118 ++++++++++++++++++++ 2 files changed, 249 insertions(+), 13 deletions(-) diff --git a/skill/scripts/live-copy-edit-agent.mjs b/skill/scripts/live-copy-edit-agent.mjs index f70f26ff4..febc8456d 100644 --- a/skill/scripts/live-copy-edit-agent.mjs +++ b/skill/scripts/live-copy-edit-agent.mjs @@ -14,10 +14,12 @@ import path from 'node:path'; import { createRequire } from 'node:module'; const DEFAULT_TIMEOUT_MS = 60_000; +const BATCH_OP_TEXT_LIMIT = 240; const require = createRequire(import.meta.url); export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { - const repairLines = batch?.repair ? [ + const compactBatch = compactBatchForPrompt(batch); + const repairLines = compactBatch.repair ? [ '', 'Repair mode:', '- The previous Apply attempt changed source, but validation failed.', @@ -28,7 +30,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { '- If failures or candidates show edited text is also a lookup key, update coupled count, animation, icon, image, asset, style, or metadata keys in the current source, or fail that entry without partial edits.', '- Keep failed and notes as arrays.', '- Return the same canonical JSON shape after repair.', - JSON.stringify(batch.repair, null, 2), + JSON.stringify(compactBatch.repair, null, 2), ] : []; return [ 'You are the Impeccable staged copy-edit batch applier.', @@ -80,7 +82,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { ...repairLines, '', 'Staged copy-edit batch:', - JSON.stringify(compactBatchForPrompt(batch), null, 2), + JSON.stringify(compactBatch, null, 2), ].join('\n'); } @@ -292,7 +294,7 @@ function readManualEditValidationScript(cwd) { function compactBatchForPrompt(batch) { return { pageUrl: batch?.pageUrl || null, - repair: batch?.repair || undefined, + repair: compactBatchRepair(batch?.repair), entries: (batch?.entries || []).map((entry) => ({ id: entry.id, pageUrl: entry.pageUrl, @@ -300,7 +302,71 @@ function compactBatchForPrompt(batch) { element: compactContextForBatch(entry.element), ops: (entry.ops || []).map(compactBatchOp), })), - candidates: batch?.candidates || [], + candidates: compactBatchCandidates(batch?.candidates), + }; +} + +function compactBatchRepair(repair) { + if (!repair || typeof repair !== 'object') return undefined; + return { + status: compactBatchString(repair.status), + attempt: normalizeOptionalBatchNumber(repair.attempt), + attempts: normalizeOptionalBatchNumber(repair.attempts), + maxAttempts: normalizeOptionalBatchNumber(repair.maxAttempts), + reason: compactBatchString(repair.reason), + transactionId: compactBatchString(repair.transactionId), + pageUrl: compactBatchString(repair.pageUrl), + failures: compactBatchDiagnostics(repair.failures), + files: compactBatchStringList(repair.files, 20), + }; +} + +function compactBatchDiagnostics(items, depth = 0) { + if (!Array.isArray(items)) return undefined; + return items.slice(0, 12).map((item) => ({ + entryId: compactBatchString(item?.entryId || item?.id), + reason: compactBatchString(item?.reason || item?.kind), + detail: compactBatchString(item?.detail), + message: compactBatchString(item?.message), + file: compactBatchString(item?.file || item?.relativeFile), + line: normalizeOptionalBatchNumber(item?.line), + ref: compactBatchString(item?.ref), + marker: compactBatchString(item?.marker), + files: compactBatchStringList(item?.files, 8), + candidates: depth < 2 ? compactBatchSourceMatches(item?.candidates, 8) : undefined, + failures: depth < 2 ? compactBatchDiagnostics(item?.failures, depth + 1) : undefined, + checks: depth < 2 ? compactBatchDiagnostics(item?.checks, depth + 1) : undefined, + })); +} + +function compactBatchCandidates(candidates) { + return (Array.isArray(candidates) ? candidates : []) + .slice(0, 24) + .map((candidate) => ({ + entryId: compactBatchString(candidate?.entryId), + ref: compactBatchString(candidate?.ref), + sourceHint: compactBatchSourceMatch(candidate?.sourceHint), + textMatches: compactBatchSourceMatches(candidate?.textMatches, 8), + objectKeyMatches: compactBatchSourceMatches(candidate?.objectKeyMatches, 8), + contextTextMatches: compactBatchSourceMatches(candidate?.contextTextMatches, 8), + locatorMatches: compactBatchSourceMatches(candidate?.locatorMatches, 6), + })); +} + +function compactBatchSourceMatches(matches, limit) { + if (!Array.isArray(matches)) return undefined; + return matches.slice(0, limit).map(compactBatchSourceMatch).filter(Boolean); +} + +function compactBatchSourceMatch(match) { + if (!match || typeof match !== 'object') return null; + return { + file: compactBatchString(match.relativeFile || match.file), + line: normalizeBatchNumber(match.line), + column: normalizeBatchNumber(match.column), + kind: compactBatchString(match.kind), + reason: compactBatchString(match.reason || match.kind), + status: compactBatchString(match.status), }; } @@ -311,25 +377,77 @@ function compactBatchOp(op) { contextRef: op.contextRef, tag: op.tag, elementId: op.elementId, - classes: op.classes, + classes: compactBatchStringList(op.classes, 24), originalText: op.originalText, newText: op.newText, deleted: op.deleted === true || undefined, - sourceHint: op.sourceHint, + sourceHint: normalizeBatchSourceHint(op.sourceHint), leaf: compactContextForBatch(op.leaf), - nearbyEditableTexts: Array.isArray(op.nearbyEditableTexts) ? op.nearbyEditableTexts.slice(0, 8) : [], + nearbyEditableTexts: compactNearbyBatchTexts(op.nearbyEditableTexts), container: compactContextForBatch(op.container), - contextHints: Array.isArray(op.contextHints) ? op.contextHints.slice(0, 12) : [], + contextHints: compactBatchStringList(op.contextHints, 12), }; } +function normalizeBatchSourceHint(hint) { + if (!hint || typeof hint !== 'object') return null; + let line = normalizeBatchNumber(hint.line); + let column = normalizeBatchNumber(hint.column); + if ((line === null || column === null) && typeof hint.loc === 'string') { + const match = hint.loc.match(/^(\d+)(?::(\d+))?/); + if (match) { + line = Number(match[1]); + if (match[2]) column = Number(match[2]); + } + } + return { + file: compactBatchString(hint.file) || '', + loc: compactBatchString(hint.loc) || '', + line, + column, + }; +} + +function normalizeBatchNumber(value) { + if (value === null || value === undefined || value === '') return null; + const number = Number(value); + return Number.isFinite(number) ? number : null; +} + +function normalizeOptionalBatchNumber(value) { + const number = normalizeBatchNumber(value); + return number === null ? undefined : number; +} + +function compactNearbyBatchTexts(items) { + return (Array.isArray(items) ? items : []) + .slice(0, 8) + .map((item) => typeof item === 'string' ? { text: truncate(item, BATCH_OP_TEXT_LIMIT) } : { + ref: compactBatchString(item?.ref), + tag: compactBatchString(item?.tag), + classes: compactBatchStringList(item?.classes, 24), + text: compactBatchString(item?.text), + }); +} + +function compactBatchStringList(items, limit) { + return (Array.isArray(items) ? items : []) + .slice(0, limit) + .filter((item) => typeof item === 'string') + .map((item) => truncate(item, BATCH_OP_TEXT_LIMIT)); +} + +function compactBatchString(value) { + return typeof value === 'string' ? truncate(value, BATCH_OP_TEXT_LIMIT) : undefined; +} + function compactContextForBatch(value) { if (!value || typeof value !== 'object') return value || null; return { - ref: value.ref, - tagName: value.tagName, - id: value.id, - classes: value.classes, + ref: compactBatchString(value.ref), + tagName: compactBatchString(value.tagName), + id: compactBatchString(value.id), + classes: compactBatchStringList(value.classes, 24), textContent: truncate(value.textContent, 900), outerHTML: truncate(stripLiveRuntimeHtml(value.outerHTML), 1800), }; diff --git a/tests/live-copy-edit-agent.test.mjs b/tests/live-copy-edit-agent.test.mjs index 98d9c0c1b..522f494f0 100644 --- a/tests/live-copy-edit-agent.test.mjs +++ b/tests/live-copy-edit-agent.test.mjs @@ -51,6 +51,124 @@ describe('live-copy-edit-agent', () => { assert.match(prompt, /Return ONLY JSON/); }); + it('bounds and whitelists operation context in batch prompts', () => { + const huge = 'Z'.repeat(50_000); + const prompt = buildCopyEditBatchPrompt({ + pageUrl: '/', + entries: [{ + id: 'bounded', + pageUrl: '/', + ops: [{ + classes: [huge], + originalText: 'Old', + newText: 'New', + sourceHint: { + file: 'src/App.jsx', + loc: '12:3', + nested: { payload: huge }, + }, + nearbyEditableTexts: [{ + ref: 'body>main>span', + tag: 'span', + classes: ['label'], + text: huge, + extra: huge, + }], + contextHints: [huge], + }], + }], + }); + + const serializedBatch = prompt.split('Staged copy-edit batch:\n').pop(); + const op = JSON.parse(serializedBatch).entries[0].ops[0]; + assert.ok(prompt.length < 20_000, `expected compact prompt, got ${prompt.length} characters`); + assert.ok(op.classes[0].length < 400); + assert.deepEqual(op.sourceHint, { + file: 'src/App.jsx', + loc: '12:3', + line: 12, + column: 3, + }); + assert.deepEqual(Object.keys(op.nearbyEditableTexts[0]).sort(), ['classes', 'ref', 'tag', 'text']); + assert.ok(op.nearbyEditableTexts[0].text.length < 400); + assert.ok(op.contextHints[0].length < 400); + }); + + it('bounds batch repair, candidate, and element context', () => { + const huge = 'Z'.repeat(250_000); + const prompt = buildCopyEditBatchPrompt({ + pageUrl: '/', + repair: { + status: 'needs_decision', + attempt: 2, + maxAttempts: 3, + reason: 'source_verification_failed', + pageUrl: '/pricing', + transactionId: huge, + failures: [{ + entryId: 'bounded', + message: huge, + candidates: [{ file: huge, line: 12, kind: 'text' }], + failures: [{ ref: huge, reason: huge }], + checks: [{ file: huge, reason: huge }], + extra: huge, + }], + files: [huge], + extra: huge, + }, + entries: [{ + id: 'bounded', + element: { ref: huge, tagName: huge, id: huge, classes: [huge], textContent: huge }, + ops: [{ + originalText: 'Old', + newText: 'New', + sourceHint: { file: 'src/App.jsx', line: null, column: null }, + }], + }], + candidates: [{ + entryId: 'bounded', + ref: huge, + sourceHint: { file: huge, line: null, extra: huge }, + textMatches: [{ file: huge, reason: huge, extra: huge }], + extra: huge, + }], + }); + + const serializedBatch = prompt.split('Staged copy-edit batch:\n').pop(); + const compact = JSON.parse(serializedBatch); + assert.ok(prompt.length < 25_000, `expected compact prompt, got ${prompt.length} characters`); + assert.deepEqual(Object.keys(compact.repair).sort(), [ + 'failures', + 'files', + 'attempt', + 'maxAttempts', + 'pageUrl', + 'reason', + 'status', + 'transactionId', + ].sort()); + assert.equal(compact.repair.attempt, 2); + assert.equal(compact.repair.reason, 'source_verification_failed'); + assert.ok(compact.repair.transactionId.length < 400); + assert.ok(compact.repair.failures[0].message.length < 400); + assert.equal(compact.repair.failures[0].entryId, 'bounded'); + assert.ok(compact.repair.failures[0].candidates[0].file.length < 400); + assert.ok(compact.repair.failures[0].failures[0].ref.length < 400); + assert.ok(compact.repair.failures[0].checks[0].file.length < 400); + assert.deepEqual(Object.keys(compact.candidates[0]).sort(), [ + 'entryId', + 'ref', + 'sourceHint', + 'textMatches', + ]); + assert.ok(compact.candidates[0].ref.length < 400); + assert.ok(compact.candidates[0].sourceHint.file.length < 400); + assert.ok(compact.entries[0].element.ref.length < 400); + assert.ok(compact.entries[0].element.classes[0].length < 400); + assert.equal(compact.entries[0].ops[0].sourceHint.line, null); + assert.equal(compact.entries[0].ops[0].sourceHint.column, null); + }); + it('parses partial batch results', () => { assert.deepEqual( parseCopyEditBatchResult('{"status":"partial","appliedEntryIds":["a"],"failed":[{"entryId":"b","reason":"ambiguous"}],"files":["src/page.js"]}'), From 2ab054d1f400c5ec085133352232ffc2617f0d54 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 9 Aug 2026 23:57:37 +0000 Subject: [PATCH 23/29] Sync generated provider output --- .../scripts/live-copy-edit-agent.mjs | 147 ++++++++++++++++-- .../scripts/live-copy-edit-agent.mjs | 147 ++++++++++++++++-- .../scripts/live-copy-edit-agent.mjs | 147 ++++++++++++++++-- .../scripts/live-copy-edit-agent.mjs | 147 ++++++++++++++++-- .../scripts/live-copy-edit-agent.mjs | 147 ++++++++++++++++-- .../scripts/live-copy-edit-agent.mjs | 147 ++++++++++++++++-- .../scripts/live-copy-edit-agent.mjs | 147 ++++++++++++++++-- .../scripts/live-copy-edit-agent.mjs | 147 ++++++++++++++++-- .../scripts/live-copy-edit-agent.mjs | 147 ++++++++++++++++-- .../scripts/live-copy-edit-agent.mjs | 147 ++++++++++++++++-- .../scripts/live-copy-edit-agent.mjs | 147 ++++++++++++++++-- .../scripts/live-copy-edit-agent.mjs | 147 ++++++++++++++++-- .../scripts/live-copy-edit-agent.mjs | 147 ++++++++++++++++-- .../scripts/live-copy-edit-agent.mjs | 147 ++++++++++++++++-- .../scripts/live-copy-edit-agent.mjs | 147 ++++++++++++++++-- 15 files changed, 1980 insertions(+), 225 deletions(-) diff --git a/.agents/skills/impeccable/scripts/live-copy-edit-agent.mjs b/.agents/skills/impeccable/scripts/live-copy-edit-agent.mjs index 313ed7f10..febc8456d 100644 --- a/.agents/skills/impeccable/scripts/live-copy-edit-agent.mjs +++ b/.agents/skills/impeccable/scripts/live-copy-edit-agent.mjs @@ -14,10 +14,12 @@ import path from 'node:path'; import { createRequire } from 'node:module'; const DEFAULT_TIMEOUT_MS = 60_000; +const BATCH_OP_TEXT_LIMIT = 240; const require = createRequire(import.meta.url); export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { - const repairLines = batch?.repair ? [ + const compactBatch = compactBatchForPrompt(batch); + const repairLines = compactBatch.repair ? [ '', 'Repair mode:', '- The previous Apply attempt changed source, but validation failed.', @@ -28,7 +30,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { '- If failures or candidates show edited text is also a lookup key, update coupled count, animation, icon, image, asset, style, or metadata keys in the current source, or fail that entry without partial edits.', '- Keep failed and notes as arrays.', '- Return the same canonical JSON shape after repair.', - JSON.stringify(batch.repair, null, 2), + JSON.stringify(compactBatch.repair, null, 2), ] : []; return [ 'You are the Impeccable staged copy-edit batch applier.', @@ -80,7 +82,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { ...repairLines, '', 'Staged copy-edit batch:', - JSON.stringify(compactBatchForPrompt(batch), null, 2), + JSON.stringify(compactBatch, null, 2), ].join('\n'); } @@ -292,7 +294,7 @@ function readManualEditValidationScript(cwd) { function compactBatchForPrompt(batch) { return { pageUrl: batch?.pageUrl || null, - repair: batch?.repair || undefined, + repair: compactBatchRepair(batch?.repair), entries: (batch?.entries || []).map((entry) => ({ id: entry.id, pageUrl: entry.pageUrl, @@ -300,7 +302,71 @@ function compactBatchForPrompt(batch) { element: compactContextForBatch(entry.element), ops: (entry.ops || []).map(compactBatchOp), })), - candidates: batch?.candidates || [], + candidates: compactBatchCandidates(batch?.candidates), + }; +} + +function compactBatchRepair(repair) { + if (!repair || typeof repair !== 'object') return undefined; + return { + status: compactBatchString(repair.status), + attempt: normalizeOptionalBatchNumber(repair.attempt), + attempts: normalizeOptionalBatchNumber(repair.attempts), + maxAttempts: normalizeOptionalBatchNumber(repair.maxAttempts), + reason: compactBatchString(repair.reason), + transactionId: compactBatchString(repair.transactionId), + pageUrl: compactBatchString(repair.pageUrl), + failures: compactBatchDiagnostics(repair.failures), + files: compactBatchStringList(repair.files, 20), + }; +} + +function compactBatchDiagnostics(items, depth = 0) { + if (!Array.isArray(items)) return undefined; + return items.slice(0, 12).map((item) => ({ + entryId: compactBatchString(item?.entryId || item?.id), + reason: compactBatchString(item?.reason || item?.kind), + detail: compactBatchString(item?.detail), + message: compactBatchString(item?.message), + file: compactBatchString(item?.file || item?.relativeFile), + line: normalizeOptionalBatchNumber(item?.line), + ref: compactBatchString(item?.ref), + marker: compactBatchString(item?.marker), + files: compactBatchStringList(item?.files, 8), + candidates: depth < 2 ? compactBatchSourceMatches(item?.candidates, 8) : undefined, + failures: depth < 2 ? compactBatchDiagnostics(item?.failures, depth + 1) : undefined, + checks: depth < 2 ? compactBatchDiagnostics(item?.checks, depth + 1) : undefined, + })); +} + +function compactBatchCandidates(candidates) { + return (Array.isArray(candidates) ? candidates : []) + .slice(0, 24) + .map((candidate) => ({ + entryId: compactBatchString(candidate?.entryId), + ref: compactBatchString(candidate?.ref), + sourceHint: compactBatchSourceMatch(candidate?.sourceHint), + textMatches: compactBatchSourceMatches(candidate?.textMatches, 8), + objectKeyMatches: compactBatchSourceMatches(candidate?.objectKeyMatches, 8), + contextTextMatches: compactBatchSourceMatches(candidate?.contextTextMatches, 8), + locatorMatches: compactBatchSourceMatches(candidate?.locatorMatches, 6), + })); +} + +function compactBatchSourceMatches(matches, limit) { + if (!Array.isArray(matches)) return undefined; + return matches.slice(0, limit).map(compactBatchSourceMatch).filter(Boolean); +} + +function compactBatchSourceMatch(match) { + if (!match || typeof match !== 'object') return null; + return { + file: compactBatchString(match.relativeFile || match.file), + line: normalizeBatchNumber(match.line), + column: normalizeBatchNumber(match.column), + kind: compactBatchString(match.kind), + reason: compactBatchString(match.reason || match.kind), + status: compactBatchString(match.status), }; } @@ -311,25 +377,77 @@ function compactBatchOp(op) { contextRef: op.contextRef, tag: op.tag, elementId: op.elementId, - classes: op.classes, + classes: compactBatchStringList(op.classes, 24), originalText: op.originalText, newText: op.newText, deleted: op.deleted === true || undefined, - sourceHint: op.sourceHint, + sourceHint: normalizeBatchSourceHint(op.sourceHint), leaf: compactContextForBatch(op.leaf), - nearbyEditableTexts: Array.isArray(op.nearbyEditableTexts) ? op.nearbyEditableTexts.slice(0, 8) : [], + nearbyEditableTexts: compactNearbyBatchTexts(op.nearbyEditableTexts), container: compactContextForBatch(op.container), - contextHints: Array.isArray(op.contextHints) ? op.contextHints.slice(0, 12) : [], + contextHints: compactBatchStringList(op.contextHints, 12), }; } +function normalizeBatchSourceHint(hint) { + if (!hint || typeof hint !== 'object') return null; + let line = normalizeBatchNumber(hint.line); + let column = normalizeBatchNumber(hint.column); + if ((line === null || column === null) && typeof hint.loc === 'string') { + const match = hint.loc.match(/^(\d+)(?::(\d+))?/); + if (match) { + line = Number(match[1]); + if (match[2]) column = Number(match[2]); + } + } + return { + file: compactBatchString(hint.file) || '', + loc: compactBatchString(hint.loc) || '', + line, + column, + }; +} + +function normalizeBatchNumber(value) { + if (value === null || value === undefined || value === '') return null; + const number = Number(value); + return Number.isFinite(number) ? number : null; +} + +function normalizeOptionalBatchNumber(value) { + const number = normalizeBatchNumber(value); + return number === null ? undefined : number; +} + +function compactNearbyBatchTexts(items) { + return (Array.isArray(items) ? items : []) + .slice(0, 8) + .map((item) => typeof item === 'string' ? { text: truncate(item, BATCH_OP_TEXT_LIMIT) } : { + ref: compactBatchString(item?.ref), + tag: compactBatchString(item?.tag), + classes: compactBatchStringList(item?.classes, 24), + text: compactBatchString(item?.text), + }); +} + +function compactBatchStringList(items, limit) { + return (Array.isArray(items) ? items : []) + .slice(0, limit) + .filter((item) => typeof item === 'string') + .map((item) => truncate(item, BATCH_OP_TEXT_LIMIT)); +} + +function compactBatchString(value) { + return typeof value === 'string' ? truncate(value, BATCH_OP_TEXT_LIMIT) : undefined; +} + function compactContextForBatch(value) { if (!value || typeof value !== 'object') return value || null; return { - ref: value.ref, - tagName: value.tagName, - id: value.id, - classes: value.classes, + ref: compactBatchString(value.ref), + tagName: compactBatchString(value.tagName), + id: compactBatchString(value.id), + classes: compactBatchStringList(value.classes, 24), textContent: truncate(value.textContent, 900), outerHTML: truncate(stripLiveRuntimeHtml(value.outerHTML), 1800), }; @@ -470,12 +588,11 @@ function runClaude(prompt, { cwd, env, resultPath, logPath, timeoutMs = DEFAULT_ if (env.IMPECCABLE_LIVE_COPY_AGENT_MODEL) { args.push('--model', env.IMPECCABLE_LIVE_COPY_AGENT_MODEL); } - args.push(prompt); // Forward env as-is so CLAUDE_CODE_OAUTH_TOKEN and ANTHROPIC_API_KEY flow // through. On macOS, `claude /login` stores creds in the Keychain, which a // non-TTY subprocess cannot read; setting CLAUDE_CODE_OAUTH_TOKEN (via // `claude setup-token`) is the supported headless auth path. - return runAgentProcess('claude', args, '', { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); + return runAgentProcess('claude', args, prompt, { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); } function runAgentProcess(command, args, stdin, { cwd, env, logPath, timeoutMs, mirrorOutputPath }) { diff --git a/.claude/skills/impeccable/scripts/live-copy-edit-agent.mjs b/.claude/skills/impeccable/scripts/live-copy-edit-agent.mjs index 313ed7f10..febc8456d 100644 --- a/.claude/skills/impeccable/scripts/live-copy-edit-agent.mjs +++ b/.claude/skills/impeccable/scripts/live-copy-edit-agent.mjs @@ -14,10 +14,12 @@ import path from 'node:path'; import { createRequire } from 'node:module'; const DEFAULT_TIMEOUT_MS = 60_000; +const BATCH_OP_TEXT_LIMIT = 240; const require = createRequire(import.meta.url); export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { - const repairLines = batch?.repair ? [ + const compactBatch = compactBatchForPrompt(batch); + const repairLines = compactBatch.repair ? [ '', 'Repair mode:', '- The previous Apply attempt changed source, but validation failed.', @@ -28,7 +30,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { '- If failures or candidates show edited text is also a lookup key, update coupled count, animation, icon, image, asset, style, or metadata keys in the current source, or fail that entry without partial edits.', '- Keep failed and notes as arrays.', '- Return the same canonical JSON shape after repair.', - JSON.stringify(batch.repair, null, 2), + JSON.stringify(compactBatch.repair, null, 2), ] : []; return [ 'You are the Impeccable staged copy-edit batch applier.', @@ -80,7 +82,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { ...repairLines, '', 'Staged copy-edit batch:', - JSON.stringify(compactBatchForPrompt(batch), null, 2), + JSON.stringify(compactBatch, null, 2), ].join('\n'); } @@ -292,7 +294,7 @@ function readManualEditValidationScript(cwd) { function compactBatchForPrompt(batch) { return { pageUrl: batch?.pageUrl || null, - repair: batch?.repair || undefined, + repair: compactBatchRepair(batch?.repair), entries: (batch?.entries || []).map((entry) => ({ id: entry.id, pageUrl: entry.pageUrl, @@ -300,7 +302,71 @@ function compactBatchForPrompt(batch) { element: compactContextForBatch(entry.element), ops: (entry.ops || []).map(compactBatchOp), })), - candidates: batch?.candidates || [], + candidates: compactBatchCandidates(batch?.candidates), + }; +} + +function compactBatchRepair(repair) { + if (!repair || typeof repair !== 'object') return undefined; + return { + status: compactBatchString(repair.status), + attempt: normalizeOptionalBatchNumber(repair.attempt), + attempts: normalizeOptionalBatchNumber(repair.attempts), + maxAttempts: normalizeOptionalBatchNumber(repair.maxAttempts), + reason: compactBatchString(repair.reason), + transactionId: compactBatchString(repair.transactionId), + pageUrl: compactBatchString(repair.pageUrl), + failures: compactBatchDiagnostics(repair.failures), + files: compactBatchStringList(repair.files, 20), + }; +} + +function compactBatchDiagnostics(items, depth = 0) { + if (!Array.isArray(items)) return undefined; + return items.slice(0, 12).map((item) => ({ + entryId: compactBatchString(item?.entryId || item?.id), + reason: compactBatchString(item?.reason || item?.kind), + detail: compactBatchString(item?.detail), + message: compactBatchString(item?.message), + file: compactBatchString(item?.file || item?.relativeFile), + line: normalizeOptionalBatchNumber(item?.line), + ref: compactBatchString(item?.ref), + marker: compactBatchString(item?.marker), + files: compactBatchStringList(item?.files, 8), + candidates: depth < 2 ? compactBatchSourceMatches(item?.candidates, 8) : undefined, + failures: depth < 2 ? compactBatchDiagnostics(item?.failures, depth + 1) : undefined, + checks: depth < 2 ? compactBatchDiagnostics(item?.checks, depth + 1) : undefined, + })); +} + +function compactBatchCandidates(candidates) { + return (Array.isArray(candidates) ? candidates : []) + .slice(0, 24) + .map((candidate) => ({ + entryId: compactBatchString(candidate?.entryId), + ref: compactBatchString(candidate?.ref), + sourceHint: compactBatchSourceMatch(candidate?.sourceHint), + textMatches: compactBatchSourceMatches(candidate?.textMatches, 8), + objectKeyMatches: compactBatchSourceMatches(candidate?.objectKeyMatches, 8), + contextTextMatches: compactBatchSourceMatches(candidate?.contextTextMatches, 8), + locatorMatches: compactBatchSourceMatches(candidate?.locatorMatches, 6), + })); +} + +function compactBatchSourceMatches(matches, limit) { + if (!Array.isArray(matches)) return undefined; + return matches.slice(0, limit).map(compactBatchSourceMatch).filter(Boolean); +} + +function compactBatchSourceMatch(match) { + if (!match || typeof match !== 'object') return null; + return { + file: compactBatchString(match.relativeFile || match.file), + line: normalizeBatchNumber(match.line), + column: normalizeBatchNumber(match.column), + kind: compactBatchString(match.kind), + reason: compactBatchString(match.reason || match.kind), + status: compactBatchString(match.status), }; } @@ -311,25 +377,77 @@ function compactBatchOp(op) { contextRef: op.contextRef, tag: op.tag, elementId: op.elementId, - classes: op.classes, + classes: compactBatchStringList(op.classes, 24), originalText: op.originalText, newText: op.newText, deleted: op.deleted === true || undefined, - sourceHint: op.sourceHint, + sourceHint: normalizeBatchSourceHint(op.sourceHint), leaf: compactContextForBatch(op.leaf), - nearbyEditableTexts: Array.isArray(op.nearbyEditableTexts) ? op.nearbyEditableTexts.slice(0, 8) : [], + nearbyEditableTexts: compactNearbyBatchTexts(op.nearbyEditableTexts), container: compactContextForBatch(op.container), - contextHints: Array.isArray(op.contextHints) ? op.contextHints.slice(0, 12) : [], + contextHints: compactBatchStringList(op.contextHints, 12), }; } +function normalizeBatchSourceHint(hint) { + if (!hint || typeof hint !== 'object') return null; + let line = normalizeBatchNumber(hint.line); + let column = normalizeBatchNumber(hint.column); + if ((line === null || column === null) && typeof hint.loc === 'string') { + const match = hint.loc.match(/^(\d+)(?::(\d+))?/); + if (match) { + line = Number(match[1]); + if (match[2]) column = Number(match[2]); + } + } + return { + file: compactBatchString(hint.file) || '', + loc: compactBatchString(hint.loc) || '', + line, + column, + }; +} + +function normalizeBatchNumber(value) { + if (value === null || value === undefined || value === '') return null; + const number = Number(value); + return Number.isFinite(number) ? number : null; +} + +function normalizeOptionalBatchNumber(value) { + const number = normalizeBatchNumber(value); + return number === null ? undefined : number; +} + +function compactNearbyBatchTexts(items) { + return (Array.isArray(items) ? items : []) + .slice(0, 8) + .map((item) => typeof item === 'string' ? { text: truncate(item, BATCH_OP_TEXT_LIMIT) } : { + ref: compactBatchString(item?.ref), + tag: compactBatchString(item?.tag), + classes: compactBatchStringList(item?.classes, 24), + text: compactBatchString(item?.text), + }); +} + +function compactBatchStringList(items, limit) { + return (Array.isArray(items) ? items : []) + .slice(0, limit) + .filter((item) => typeof item === 'string') + .map((item) => truncate(item, BATCH_OP_TEXT_LIMIT)); +} + +function compactBatchString(value) { + return typeof value === 'string' ? truncate(value, BATCH_OP_TEXT_LIMIT) : undefined; +} + function compactContextForBatch(value) { if (!value || typeof value !== 'object') return value || null; return { - ref: value.ref, - tagName: value.tagName, - id: value.id, - classes: value.classes, + ref: compactBatchString(value.ref), + tagName: compactBatchString(value.tagName), + id: compactBatchString(value.id), + classes: compactBatchStringList(value.classes, 24), textContent: truncate(value.textContent, 900), outerHTML: truncate(stripLiveRuntimeHtml(value.outerHTML), 1800), }; @@ -470,12 +588,11 @@ function runClaude(prompt, { cwd, env, resultPath, logPath, timeoutMs = DEFAULT_ if (env.IMPECCABLE_LIVE_COPY_AGENT_MODEL) { args.push('--model', env.IMPECCABLE_LIVE_COPY_AGENT_MODEL); } - args.push(prompt); // Forward env as-is so CLAUDE_CODE_OAUTH_TOKEN and ANTHROPIC_API_KEY flow // through. On macOS, `claude /login` stores creds in the Keychain, which a // non-TTY subprocess cannot read; setting CLAUDE_CODE_OAUTH_TOKEN (via // `claude setup-token`) is the supported headless auth path. - return runAgentProcess('claude', args, '', { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); + return runAgentProcess('claude', args, prompt, { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); } function runAgentProcess(command, args, stdin, { cwd, env, logPath, timeoutMs, mirrorOutputPath }) { diff --git a/.cursor/skills/impeccable/scripts/live-copy-edit-agent.mjs b/.cursor/skills/impeccable/scripts/live-copy-edit-agent.mjs index 313ed7f10..febc8456d 100644 --- a/.cursor/skills/impeccable/scripts/live-copy-edit-agent.mjs +++ b/.cursor/skills/impeccable/scripts/live-copy-edit-agent.mjs @@ -14,10 +14,12 @@ import path from 'node:path'; import { createRequire } from 'node:module'; const DEFAULT_TIMEOUT_MS = 60_000; +const BATCH_OP_TEXT_LIMIT = 240; const require = createRequire(import.meta.url); export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { - const repairLines = batch?.repair ? [ + const compactBatch = compactBatchForPrompt(batch); + const repairLines = compactBatch.repair ? [ '', 'Repair mode:', '- The previous Apply attempt changed source, but validation failed.', @@ -28,7 +30,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { '- If failures or candidates show edited text is also a lookup key, update coupled count, animation, icon, image, asset, style, or metadata keys in the current source, or fail that entry without partial edits.', '- Keep failed and notes as arrays.', '- Return the same canonical JSON shape after repair.', - JSON.stringify(batch.repair, null, 2), + JSON.stringify(compactBatch.repair, null, 2), ] : []; return [ 'You are the Impeccable staged copy-edit batch applier.', @@ -80,7 +82,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { ...repairLines, '', 'Staged copy-edit batch:', - JSON.stringify(compactBatchForPrompt(batch), null, 2), + JSON.stringify(compactBatch, null, 2), ].join('\n'); } @@ -292,7 +294,7 @@ function readManualEditValidationScript(cwd) { function compactBatchForPrompt(batch) { return { pageUrl: batch?.pageUrl || null, - repair: batch?.repair || undefined, + repair: compactBatchRepair(batch?.repair), entries: (batch?.entries || []).map((entry) => ({ id: entry.id, pageUrl: entry.pageUrl, @@ -300,7 +302,71 @@ function compactBatchForPrompt(batch) { element: compactContextForBatch(entry.element), ops: (entry.ops || []).map(compactBatchOp), })), - candidates: batch?.candidates || [], + candidates: compactBatchCandidates(batch?.candidates), + }; +} + +function compactBatchRepair(repair) { + if (!repair || typeof repair !== 'object') return undefined; + return { + status: compactBatchString(repair.status), + attempt: normalizeOptionalBatchNumber(repair.attempt), + attempts: normalizeOptionalBatchNumber(repair.attempts), + maxAttempts: normalizeOptionalBatchNumber(repair.maxAttempts), + reason: compactBatchString(repair.reason), + transactionId: compactBatchString(repair.transactionId), + pageUrl: compactBatchString(repair.pageUrl), + failures: compactBatchDiagnostics(repair.failures), + files: compactBatchStringList(repair.files, 20), + }; +} + +function compactBatchDiagnostics(items, depth = 0) { + if (!Array.isArray(items)) return undefined; + return items.slice(0, 12).map((item) => ({ + entryId: compactBatchString(item?.entryId || item?.id), + reason: compactBatchString(item?.reason || item?.kind), + detail: compactBatchString(item?.detail), + message: compactBatchString(item?.message), + file: compactBatchString(item?.file || item?.relativeFile), + line: normalizeOptionalBatchNumber(item?.line), + ref: compactBatchString(item?.ref), + marker: compactBatchString(item?.marker), + files: compactBatchStringList(item?.files, 8), + candidates: depth < 2 ? compactBatchSourceMatches(item?.candidates, 8) : undefined, + failures: depth < 2 ? compactBatchDiagnostics(item?.failures, depth + 1) : undefined, + checks: depth < 2 ? compactBatchDiagnostics(item?.checks, depth + 1) : undefined, + })); +} + +function compactBatchCandidates(candidates) { + return (Array.isArray(candidates) ? candidates : []) + .slice(0, 24) + .map((candidate) => ({ + entryId: compactBatchString(candidate?.entryId), + ref: compactBatchString(candidate?.ref), + sourceHint: compactBatchSourceMatch(candidate?.sourceHint), + textMatches: compactBatchSourceMatches(candidate?.textMatches, 8), + objectKeyMatches: compactBatchSourceMatches(candidate?.objectKeyMatches, 8), + contextTextMatches: compactBatchSourceMatches(candidate?.contextTextMatches, 8), + locatorMatches: compactBatchSourceMatches(candidate?.locatorMatches, 6), + })); +} + +function compactBatchSourceMatches(matches, limit) { + if (!Array.isArray(matches)) return undefined; + return matches.slice(0, limit).map(compactBatchSourceMatch).filter(Boolean); +} + +function compactBatchSourceMatch(match) { + if (!match || typeof match !== 'object') return null; + return { + file: compactBatchString(match.relativeFile || match.file), + line: normalizeBatchNumber(match.line), + column: normalizeBatchNumber(match.column), + kind: compactBatchString(match.kind), + reason: compactBatchString(match.reason || match.kind), + status: compactBatchString(match.status), }; } @@ -311,25 +377,77 @@ function compactBatchOp(op) { contextRef: op.contextRef, tag: op.tag, elementId: op.elementId, - classes: op.classes, + classes: compactBatchStringList(op.classes, 24), originalText: op.originalText, newText: op.newText, deleted: op.deleted === true || undefined, - sourceHint: op.sourceHint, + sourceHint: normalizeBatchSourceHint(op.sourceHint), leaf: compactContextForBatch(op.leaf), - nearbyEditableTexts: Array.isArray(op.nearbyEditableTexts) ? op.nearbyEditableTexts.slice(0, 8) : [], + nearbyEditableTexts: compactNearbyBatchTexts(op.nearbyEditableTexts), container: compactContextForBatch(op.container), - contextHints: Array.isArray(op.contextHints) ? op.contextHints.slice(0, 12) : [], + contextHints: compactBatchStringList(op.contextHints, 12), }; } +function normalizeBatchSourceHint(hint) { + if (!hint || typeof hint !== 'object') return null; + let line = normalizeBatchNumber(hint.line); + let column = normalizeBatchNumber(hint.column); + if ((line === null || column === null) && typeof hint.loc === 'string') { + const match = hint.loc.match(/^(\d+)(?::(\d+))?/); + if (match) { + line = Number(match[1]); + if (match[2]) column = Number(match[2]); + } + } + return { + file: compactBatchString(hint.file) || '', + loc: compactBatchString(hint.loc) || '', + line, + column, + }; +} + +function normalizeBatchNumber(value) { + if (value === null || value === undefined || value === '') return null; + const number = Number(value); + return Number.isFinite(number) ? number : null; +} + +function normalizeOptionalBatchNumber(value) { + const number = normalizeBatchNumber(value); + return number === null ? undefined : number; +} + +function compactNearbyBatchTexts(items) { + return (Array.isArray(items) ? items : []) + .slice(0, 8) + .map((item) => typeof item === 'string' ? { text: truncate(item, BATCH_OP_TEXT_LIMIT) } : { + ref: compactBatchString(item?.ref), + tag: compactBatchString(item?.tag), + classes: compactBatchStringList(item?.classes, 24), + text: compactBatchString(item?.text), + }); +} + +function compactBatchStringList(items, limit) { + return (Array.isArray(items) ? items : []) + .slice(0, limit) + .filter((item) => typeof item === 'string') + .map((item) => truncate(item, BATCH_OP_TEXT_LIMIT)); +} + +function compactBatchString(value) { + return typeof value === 'string' ? truncate(value, BATCH_OP_TEXT_LIMIT) : undefined; +} + function compactContextForBatch(value) { if (!value || typeof value !== 'object') return value || null; return { - ref: value.ref, - tagName: value.tagName, - id: value.id, - classes: value.classes, + ref: compactBatchString(value.ref), + tagName: compactBatchString(value.tagName), + id: compactBatchString(value.id), + classes: compactBatchStringList(value.classes, 24), textContent: truncate(value.textContent, 900), outerHTML: truncate(stripLiveRuntimeHtml(value.outerHTML), 1800), }; @@ -470,12 +588,11 @@ function runClaude(prompt, { cwd, env, resultPath, logPath, timeoutMs = DEFAULT_ if (env.IMPECCABLE_LIVE_COPY_AGENT_MODEL) { args.push('--model', env.IMPECCABLE_LIVE_COPY_AGENT_MODEL); } - args.push(prompt); // Forward env as-is so CLAUDE_CODE_OAUTH_TOKEN and ANTHROPIC_API_KEY flow // through. On macOS, `claude /login` stores creds in the Keychain, which a // non-TTY subprocess cannot read; setting CLAUDE_CODE_OAUTH_TOKEN (via // `claude setup-token`) is the supported headless auth path. - return runAgentProcess('claude', args, '', { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); + return runAgentProcess('claude', args, prompt, { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); } function runAgentProcess(command, args, stdin, { cwd, env, logPath, timeoutMs, mirrorOutputPath }) { diff --git a/.gemini/skills/impeccable/scripts/live-copy-edit-agent.mjs b/.gemini/skills/impeccable/scripts/live-copy-edit-agent.mjs index 313ed7f10..febc8456d 100644 --- a/.gemini/skills/impeccable/scripts/live-copy-edit-agent.mjs +++ b/.gemini/skills/impeccable/scripts/live-copy-edit-agent.mjs @@ -14,10 +14,12 @@ import path from 'node:path'; import { createRequire } from 'node:module'; const DEFAULT_TIMEOUT_MS = 60_000; +const BATCH_OP_TEXT_LIMIT = 240; const require = createRequire(import.meta.url); export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { - const repairLines = batch?.repair ? [ + const compactBatch = compactBatchForPrompt(batch); + const repairLines = compactBatch.repair ? [ '', 'Repair mode:', '- The previous Apply attempt changed source, but validation failed.', @@ -28,7 +30,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { '- If failures or candidates show edited text is also a lookup key, update coupled count, animation, icon, image, asset, style, or metadata keys in the current source, or fail that entry without partial edits.', '- Keep failed and notes as arrays.', '- Return the same canonical JSON shape after repair.', - JSON.stringify(batch.repair, null, 2), + JSON.stringify(compactBatch.repair, null, 2), ] : []; return [ 'You are the Impeccable staged copy-edit batch applier.', @@ -80,7 +82,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { ...repairLines, '', 'Staged copy-edit batch:', - JSON.stringify(compactBatchForPrompt(batch), null, 2), + JSON.stringify(compactBatch, null, 2), ].join('\n'); } @@ -292,7 +294,7 @@ function readManualEditValidationScript(cwd) { function compactBatchForPrompt(batch) { return { pageUrl: batch?.pageUrl || null, - repair: batch?.repair || undefined, + repair: compactBatchRepair(batch?.repair), entries: (batch?.entries || []).map((entry) => ({ id: entry.id, pageUrl: entry.pageUrl, @@ -300,7 +302,71 @@ function compactBatchForPrompt(batch) { element: compactContextForBatch(entry.element), ops: (entry.ops || []).map(compactBatchOp), })), - candidates: batch?.candidates || [], + candidates: compactBatchCandidates(batch?.candidates), + }; +} + +function compactBatchRepair(repair) { + if (!repair || typeof repair !== 'object') return undefined; + return { + status: compactBatchString(repair.status), + attempt: normalizeOptionalBatchNumber(repair.attempt), + attempts: normalizeOptionalBatchNumber(repair.attempts), + maxAttempts: normalizeOptionalBatchNumber(repair.maxAttempts), + reason: compactBatchString(repair.reason), + transactionId: compactBatchString(repair.transactionId), + pageUrl: compactBatchString(repair.pageUrl), + failures: compactBatchDiagnostics(repair.failures), + files: compactBatchStringList(repair.files, 20), + }; +} + +function compactBatchDiagnostics(items, depth = 0) { + if (!Array.isArray(items)) return undefined; + return items.slice(0, 12).map((item) => ({ + entryId: compactBatchString(item?.entryId || item?.id), + reason: compactBatchString(item?.reason || item?.kind), + detail: compactBatchString(item?.detail), + message: compactBatchString(item?.message), + file: compactBatchString(item?.file || item?.relativeFile), + line: normalizeOptionalBatchNumber(item?.line), + ref: compactBatchString(item?.ref), + marker: compactBatchString(item?.marker), + files: compactBatchStringList(item?.files, 8), + candidates: depth < 2 ? compactBatchSourceMatches(item?.candidates, 8) : undefined, + failures: depth < 2 ? compactBatchDiagnostics(item?.failures, depth + 1) : undefined, + checks: depth < 2 ? compactBatchDiagnostics(item?.checks, depth + 1) : undefined, + })); +} + +function compactBatchCandidates(candidates) { + return (Array.isArray(candidates) ? candidates : []) + .slice(0, 24) + .map((candidate) => ({ + entryId: compactBatchString(candidate?.entryId), + ref: compactBatchString(candidate?.ref), + sourceHint: compactBatchSourceMatch(candidate?.sourceHint), + textMatches: compactBatchSourceMatches(candidate?.textMatches, 8), + objectKeyMatches: compactBatchSourceMatches(candidate?.objectKeyMatches, 8), + contextTextMatches: compactBatchSourceMatches(candidate?.contextTextMatches, 8), + locatorMatches: compactBatchSourceMatches(candidate?.locatorMatches, 6), + })); +} + +function compactBatchSourceMatches(matches, limit) { + if (!Array.isArray(matches)) return undefined; + return matches.slice(0, limit).map(compactBatchSourceMatch).filter(Boolean); +} + +function compactBatchSourceMatch(match) { + if (!match || typeof match !== 'object') return null; + return { + file: compactBatchString(match.relativeFile || match.file), + line: normalizeBatchNumber(match.line), + column: normalizeBatchNumber(match.column), + kind: compactBatchString(match.kind), + reason: compactBatchString(match.reason || match.kind), + status: compactBatchString(match.status), }; } @@ -311,25 +377,77 @@ function compactBatchOp(op) { contextRef: op.contextRef, tag: op.tag, elementId: op.elementId, - classes: op.classes, + classes: compactBatchStringList(op.classes, 24), originalText: op.originalText, newText: op.newText, deleted: op.deleted === true || undefined, - sourceHint: op.sourceHint, + sourceHint: normalizeBatchSourceHint(op.sourceHint), leaf: compactContextForBatch(op.leaf), - nearbyEditableTexts: Array.isArray(op.nearbyEditableTexts) ? op.nearbyEditableTexts.slice(0, 8) : [], + nearbyEditableTexts: compactNearbyBatchTexts(op.nearbyEditableTexts), container: compactContextForBatch(op.container), - contextHints: Array.isArray(op.contextHints) ? op.contextHints.slice(0, 12) : [], + contextHints: compactBatchStringList(op.contextHints, 12), }; } +function normalizeBatchSourceHint(hint) { + if (!hint || typeof hint !== 'object') return null; + let line = normalizeBatchNumber(hint.line); + let column = normalizeBatchNumber(hint.column); + if ((line === null || column === null) && typeof hint.loc === 'string') { + const match = hint.loc.match(/^(\d+)(?::(\d+))?/); + if (match) { + line = Number(match[1]); + if (match[2]) column = Number(match[2]); + } + } + return { + file: compactBatchString(hint.file) || '', + loc: compactBatchString(hint.loc) || '', + line, + column, + }; +} + +function normalizeBatchNumber(value) { + if (value === null || value === undefined || value === '') return null; + const number = Number(value); + return Number.isFinite(number) ? number : null; +} + +function normalizeOptionalBatchNumber(value) { + const number = normalizeBatchNumber(value); + return number === null ? undefined : number; +} + +function compactNearbyBatchTexts(items) { + return (Array.isArray(items) ? items : []) + .slice(0, 8) + .map((item) => typeof item === 'string' ? { text: truncate(item, BATCH_OP_TEXT_LIMIT) } : { + ref: compactBatchString(item?.ref), + tag: compactBatchString(item?.tag), + classes: compactBatchStringList(item?.classes, 24), + text: compactBatchString(item?.text), + }); +} + +function compactBatchStringList(items, limit) { + return (Array.isArray(items) ? items : []) + .slice(0, limit) + .filter((item) => typeof item === 'string') + .map((item) => truncate(item, BATCH_OP_TEXT_LIMIT)); +} + +function compactBatchString(value) { + return typeof value === 'string' ? truncate(value, BATCH_OP_TEXT_LIMIT) : undefined; +} + function compactContextForBatch(value) { if (!value || typeof value !== 'object') return value || null; return { - ref: value.ref, - tagName: value.tagName, - id: value.id, - classes: value.classes, + ref: compactBatchString(value.ref), + tagName: compactBatchString(value.tagName), + id: compactBatchString(value.id), + classes: compactBatchStringList(value.classes, 24), textContent: truncate(value.textContent, 900), outerHTML: truncate(stripLiveRuntimeHtml(value.outerHTML), 1800), }; @@ -470,12 +588,11 @@ function runClaude(prompt, { cwd, env, resultPath, logPath, timeoutMs = DEFAULT_ if (env.IMPECCABLE_LIVE_COPY_AGENT_MODEL) { args.push('--model', env.IMPECCABLE_LIVE_COPY_AGENT_MODEL); } - args.push(prompt); // Forward env as-is so CLAUDE_CODE_OAUTH_TOKEN and ANTHROPIC_API_KEY flow // through. On macOS, `claude /login` stores creds in the Keychain, which a // non-TTY subprocess cannot read; setting CLAUDE_CODE_OAUTH_TOKEN (via // `claude setup-token`) is the supported headless auth path. - return runAgentProcess('claude', args, '', { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); + return runAgentProcess('claude', args, prompt, { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); } function runAgentProcess(command, args, stdin, { cwd, env, logPath, timeoutMs, mirrorOutputPath }) { diff --git a/.github/skills/impeccable/scripts/live-copy-edit-agent.mjs b/.github/skills/impeccable/scripts/live-copy-edit-agent.mjs index 313ed7f10..febc8456d 100644 --- a/.github/skills/impeccable/scripts/live-copy-edit-agent.mjs +++ b/.github/skills/impeccable/scripts/live-copy-edit-agent.mjs @@ -14,10 +14,12 @@ import path from 'node:path'; import { createRequire } from 'node:module'; const DEFAULT_TIMEOUT_MS = 60_000; +const BATCH_OP_TEXT_LIMIT = 240; const require = createRequire(import.meta.url); export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { - const repairLines = batch?.repair ? [ + const compactBatch = compactBatchForPrompt(batch); + const repairLines = compactBatch.repair ? [ '', 'Repair mode:', '- The previous Apply attempt changed source, but validation failed.', @@ -28,7 +30,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { '- If failures or candidates show edited text is also a lookup key, update coupled count, animation, icon, image, asset, style, or metadata keys in the current source, or fail that entry without partial edits.', '- Keep failed and notes as arrays.', '- Return the same canonical JSON shape after repair.', - JSON.stringify(batch.repair, null, 2), + JSON.stringify(compactBatch.repair, null, 2), ] : []; return [ 'You are the Impeccable staged copy-edit batch applier.', @@ -80,7 +82,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { ...repairLines, '', 'Staged copy-edit batch:', - JSON.stringify(compactBatchForPrompt(batch), null, 2), + JSON.stringify(compactBatch, null, 2), ].join('\n'); } @@ -292,7 +294,7 @@ function readManualEditValidationScript(cwd) { function compactBatchForPrompt(batch) { return { pageUrl: batch?.pageUrl || null, - repair: batch?.repair || undefined, + repair: compactBatchRepair(batch?.repair), entries: (batch?.entries || []).map((entry) => ({ id: entry.id, pageUrl: entry.pageUrl, @@ -300,7 +302,71 @@ function compactBatchForPrompt(batch) { element: compactContextForBatch(entry.element), ops: (entry.ops || []).map(compactBatchOp), })), - candidates: batch?.candidates || [], + candidates: compactBatchCandidates(batch?.candidates), + }; +} + +function compactBatchRepair(repair) { + if (!repair || typeof repair !== 'object') return undefined; + return { + status: compactBatchString(repair.status), + attempt: normalizeOptionalBatchNumber(repair.attempt), + attempts: normalizeOptionalBatchNumber(repair.attempts), + maxAttempts: normalizeOptionalBatchNumber(repair.maxAttempts), + reason: compactBatchString(repair.reason), + transactionId: compactBatchString(repair.transactionId), + pageUrl: compactBatchString(repair.pageUrl), + failures: compactBatchDiagnostics(repair.failures), + files: compactBatchStringList(repair.files, 20), + }; +} + +function compactBatchDiagnostics(items, depth = 0) { + if (!Array.isArray(items)) return undefined; + return items.slice(0, 12).map((item) => ({ + entryId: compactBatchString(item?.entryId || item?.id), + reason: compactBatchString(item?.reason || item?.kind), + detail: compactBatchString(item?.detail), + message: compactBatchString(item?.message), + file: compactBatchString(item?.file || item?.relativeFile), + line: normalizeOptionalBatchNumber(item?.line), + ref: compactBatchString(item?.ref), + marker: compactBatchString(item?.marker), + files: compactBatchStringList(item?.files, 8), + candidates: depth < 2 ? compactBatchSourceMatches(item?.candidates, 8) : undefined, + failures: depth < 2 ? compactBatchDiagnostics(item?.failures, depth + 1) : undefined, + checks: depth < 2 ? compactBatchDiagnostics(item?.checks, depth + 1) : undefined, + })); +} + +function compactBatchCandidates(candidates) { + return (Array.isArray(candidates) ? candidates : []) + .slice(0, 24) + .map((candidate) => ({ + entryId: compactBatchString(candidate?.entryId), + ref: compactBatchString(candidate?.ref), + sourceHint: compactBatchSourceMatch(candidate?.sourceHint), + textMatches: compactBatchSourceMatches(candidate?.textMatches, 8), + objectKeyMatches: compactBatchSourceMatches(candidate?.objectKeyMatches, 8), + contextTextMatches: compactBatchSourceMatches(candidate?.contextTextMatches, 8), + locatorMatches: compactBatchSourceMatches(candidate?.locatorMatches, 6), + })); +} + +function compactBatchSourceMatches(matches, limit) { + if (!Array.isArray(matches)) return undefined; + return matches.slice(0, limit).map(compactBatchSourceMatch).filter(Boolean); +} + +function compactBatchSourceMatch(match) { + if (!match || typeof match !== 'object') return null; + return { + file: compactBatchString(match.relativeFile || match.file), + line: normalizeBatchNumber(match.line), + column: normalizeBatchNumber(match.column), + kind: compactBatchString(match.kind), + reason: compactBatchString(match.reason || match.kind), + status: compactBatchString(match.status), }; } @@ -311,25 +377,77 @@ function compactBatchOp(op) { contextRef: op.contextRef, tag: op.tag, elementId: op.elementId, - classes: op.classes, + classes: compactBatchStringList(op.classes, 24), originalText: op.originalText, newText: op.newText, deleted: op.deleted === true || undefined, - sourceHint: op.sourceHint, + sourceHint: normalizeBatchSourceHint(op.sourceHint), leaf: compactContextForBatch(op.leaf), - nearbyEditableTexts: Array.isArray(op.nearbyEditableTexts) ? op.nearbyEditableTexts.slice(0, 8) : [], + nearbyEditableTexts: compactNearbyBatchTexts(op.nearbyEditableTexts), container: compactContextForBatch(op.container), - contextHints: Array.isArray(op.contextHints) ? op.contextHints.slice(0, 12) : [], + contextHints: compactBatchStringList(op.contextHints, 12), }; } +function normalizeBatchSourceHint(hint) { + if (!hint || typeof hint !== 'object') return null; + let line = normalizeBatchNumber(hint.line); + let column = normalizeBatchNumber(hint.column); + if ((line === null || column === null) && typeof hint.loc === 'string') { + const match = hint.loc.match(/^(\d+)(?::(\d+))?/); + if (match) { + line = Number(match[1]); + if (match[2]) column = Number(match[2]); + } + } + return { + file: compactBatchString(hint.file) || '', + loc: compactBatchString(hint.loc) || '', + line, + column, + }; +} + +function normalizeBatchNumber(value) { + if (value === null || value === undefined || value === '') return null; + const number = Number(value); + return Number.isFinite(number) ? number : null; +} + +function normalizeOptionalBatchNumber(value) { + const number = normalizeBatchNumber(value); + return number === null ? undefined : number; +} + +function compactNearbyBatchTexts(items) { + return (Array.isArray(items) ? items : []) + .slice(0, 8) + .map((item) => typeof item === 'string' ? { text: truncate(item, BATCH_OP_TEXT_LIMIT) } : { + ref: compactBatchString(item?.ref), + tag: compactBatchString(item?.tag), + classes: compactBatchStringList(item?.classes, 24), + text: compactBatchString(item?.text), + }); +} + +function compactBatchStringList(items, limit) { + return (Array.isArray(items) ? items : []) + .slice(0, limit) + .filter((item) => typeof item === 'string') + .map((item) => truncate(item, BATCH_OP_TEXT_LIMIT)); +} + +function compactBatchString(value) { + return typeof value === 'string' ? truncate(value, BATCH_OP_TEXT_LIMIT) : undefined; +} + function compactContextForBatch(value) { if (!value || typeof value !== 'object') return value || null; return { - ref: value.ref, - tagName: value.tagName, - id: value.id, - classes: value.classes, + ref: compactBatchString(value.ref), + tagName: compactBatchString(value.tagName), + id: compactBatchString(value.id), + classes: compactBatchStringList(value.classes, 24), textContent: truncate(value.textContent, 900), outerHTML: truncate(stripLiveRuntimeHtml(value.outerHTML), 1800), }; @@ -470,12 +588,11 @@ function runClaude(prompt, { cwd, env, resultPath, logPath, timeoutMs = DEFAULT_ if (env.IMPECCABLE_LIVE_COPY_AGENT_MODEL) { args.push('--model', env.IMPECCABLE_LIVE_COPY_AGENT_MODEL); } - args.push(prompt); // Forward env as-is so CLAUDE_CODE_OAUTH_TOKEN and ANTHROPIC_API_KEY flow // through. On macOS, `claude /login` stores creds in the Keychain, which a // non-TTY subprocess cannot read; setting CLAUDE_CODE_OAUTH_TOKEN (via // `claude setup-token`) is the supported headless auth path. - return runAgentProcess('claude', args, '', { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); + return runAgentProcess('claude', args, prompt, { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); } function runAgentProcess(command, args, stdin, { cwd, env, logPath, timeoutMs, mirrorOutputPath }) { diff --git a/.grok/skills/impeccable/scripts/live-copy-edit-agent.mjs b/.grok/skills/impeccable/scripts/live-copy-edit-agent.mjs index 313ed7f10..febc8456d 100644 --- a/.grok/skills/impeccable/scripts/live-copy-edit-agent.mjs +++ b/.grok/skills/impeccable/scripts/live-copy-edit-agent.mjs @@ -14,10 +14,12 @@ import path from 'node:path'; import { createRequire } from 'node:module'; const DEFAULT_TIMEOUT_MS = 60_000; +const BATCH_OP_TEXT_LIMIT = 240; const require = createRequire(import.meta.url); export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { - const repairLines = batch?.repair ? [ + const compactBatch = compactBatchForPrompt(batch); + const repairLines = compactBatch.repair ? [ '', 'Repair mode:', '- The previous Apply attempt changed source, but validation failed.', @@ -28,7 +30,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { '- If failures or candidates show edited text is also a lookup key, update coupled count, animation, icon, image, asset, style, or metadata keys in the current source, or fail that entry without partial edits.', '- Keep failed and notes as arrays.', '- Return the same canonical JSON shape after repair.', - JSON.stringify(batch.repair, null, 2), + JSON.stringify(compactBatch.repair, null, 2), ] : []; return [ 'You are the Impeccable staged copy-edit batch applier.', @@ -80,7 +82,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { ...repairLines, '', 'Staged copy-edit batch:', - JSON.stringify(compactBatchForPrompt(batch), null, 2), + JSON.stringify(compactBatch, null, 2), ].join('\n'); } @@ -292,7 +294,7 @@ function readManualEditValidationScript(cwd) { function compactBatchForPrompt(batch) { return { pageUrl: batch?.pageUrl || null, - repair: batch?.repair || undefined, + repair: compactBatchRepair(batch?.repair), entries: (batch?.entries || []).map((entry) => ({ id: entry.id, pageUrl: entry.pageUrl, @@ -300,7 +302,71 @@ function compactBatchForPrompt(batch) { element: compactContextForBatch(entry.element), ops: (entry.ops || []).map(compactBatchOp), })), - candidates: batch?.candidates || [], + candidates: compactBatchCandidates(batch?.candidates), + }; +} + +function compactBatchRepair(repair) { + if (!repair || typeof repair !== 'object') return undefined; + return { + status: compactBatchString(repair.status), + attempt: normalizeOptionalBatchNumber(repair.attempt), + attempts: normalizeOptionalBatchNumber(repair.attempts), + maxAttempts: normalizeOptionalBatchNumber(repair.maxAttempts), + reason: compactBatchString(repair.reason), + transactionId: compactBatchString(repair.transactionId), + pageUrl: compactBatchString(repair.pageUrl), + failures: compactBatchDiagnostics(repair.failures), + files: compactBatchStringList(repair.files, 20), + }; +} + +function compactBatchDiagnostics(items, depth = 0) { + if (!Array.isArray(items)) return undefined; + return items.slice(0, 12).map((item) => ({ + entryId: compactBatchString(item?.entryId || item?.id), + reason: compactBatchString(item?.reason || item?.kind), + detail: compactBatchString(item?.detail), + message: compactBatchString(item?.message), + file: compactBatchString(item?.file || item?.relativeFile), + line: normalizeOptionalBatchNumber(item?.line), + ref: compactBatchString(item?.ref), + marker: compactBatchString(item?.marker), + files: compactBatchStringList(item?.files, 8), + candidates: depth < 2 ? compactBatchSourceMatches(item?.candidates, 8) : undefined, + failures: depth < 2 ? compactBatchDiagnostics(item?.failures, depth + 1) : undefined, + checks: depth < 2 ? compactBatchDiagnostics(item?.checks, depth + 1) : undefined, + })); +} + +function compactBatchCandidates(candidates) { + return (Array.isArray(candidates) ? candidates : []) + .slice(0, 24) + .map((candidate) => ({ + entryId: compactBatchString(candidate?.entryId), + ref: compactBatchString(candidate?.ref), + sourceHint: compactBatchSourceMatch(candidate?.sourceHint), + textMatches: compactBatchSourceMatches(candidate?.textMatches, 8), + objectKeyMatches: compactBatchSourceMatches(candidate?.objectKeyMatches, 8), + contextTextMatches: compactBatchSourceMatches(candidate?.contextTextMatches, 8), + locatorMatches: compactBatchSourceMatches(candidate?.locatorMatches, 6), + })); +} + +function compactBatchSourceMatches(matches, limit) { + if (!Array.isArray(matches)) return undefined; + return matches.slice(0, limit).map(compactBatchSourceMatch).filter(Boolean); +} + +function compactBatchSourceMatch(match) { + if (!match || typeof match !== 'object') return null; + return { + file: compactBatchString(match.relativeFile || match.file), + line: normalizeBatchNumber(match.line), + column: normalizeBatchNumber(match.column), + kind: compactBatchString(match.kind), + reason: compactBatchString(match.reason || match.kind), + status: compactBatchString(match.status), }; } @@ -311,25 +377,77 @@ function compactBatchOp(op) { contextRef: op.contextRef, tag: op.tag, elementId: op.elementId, - classes: op.classes, + classes: compactBatchStringList(op.classes, 24), originalText: op.originalText, newText: op.newText, deleted: op.deleted === true || undefined, - sourceHint: op.sourceHint, + sourceHint: normalizeBatchSourceHint(op.sourceHint), leaf: compactContextForBatch(op.leaf), - nearbyEditableTexts: Array.isArray(op.nearbyEditableTexts) ? op.nearbyEditableTexts.slice(0, 8) : [], + nearbyEditableTexts: compactNearbyBatchTexts(op.nearbyEditableTexts), container: compactContextForBatch(op.container), - contextHints: Array.isArray(op.contextHints) ? op.contextHints.slice(0, 12) : [], + contextHints: compactBatchStringList(op.contextHints, 12), }; } +function normalizeBatchSourceHint(hint) { + if (!hint || typeof hint !== 'object') return null; + let line = normalizeBatchNumber(hint.line); + let column = normalizeBatchNumber(hint.column); + if ((line === null || column === null) && typeof hint.loc === 'string') { + const match = hint.loc.match(/^(\d+)(?::(\d+))?/); + if (match) { + line = Number(match[1]); + if (match[2]) column = Number(match[2]); + } + } + return { + file: compactBatchString(hint.file) || '', + loc: compactBatchString(hint.loc) || '', + line, + column, + }; +} + +function normalizeBatchNumber(value) { + if (value === null || value === undefined || value === '') return null; + const number = Number(value); + return Number.isFinite(number) ? number : null; +} + +function normalizeOptionalBatchNumber(value) { + const number = normalizeBatchNumber(value); + return number === null ? undefined : number; +} + +function compactNearbyBatchTexts(items) { + return (Array.isArray(items) ? items : []) + .slice(0, 8) + .map((item) => typeof item === 'string' ? { text: truncate(item, BATCH_OP_TEXT_LIMIT) } : { + ref: compactBatchString(item?.ref), + tag: compactBatchString(item?.tag), + classes: compactBatchStringList(item?.classes, 24), + text: compactBatchString(item?.text), + }); +} + +function compactBatchStringList(items, limit) { + return (Array.isArray(items) ? items : []) + .slice(0, limit) + .filter((item) => typeof item === 'string') + .map((item) => truncate(item, BATCH_OP_TEXT_LIMIT)); +} + +function compactBatchString(value) { + return typeof value === 'string' ? truncate(value, BATCH_OP_TEXT_LIMIT) : undefined; +} + function compactContextForBatch(value) { if (!value || typeof value !== 'object') return value || null; return { - ref: value.ref, - tagName: value.tagName, - id: value.id, - classes: value.classes, + ref: compactBatchString(value.ref), + tagName: compactBatchString(value.tagName), + id: compactBatchString(value.id), + classes: compactBatchStringList(value.classes, 24), textContent: truncate(value.textContent, 900), outerHTML: truncate(stripLiveRuntimeHtml(value.outerHTML), 1800), }; @@ -470,12 +588,11 @@ function runClaude(prompt, { cwd, env, resultPath, logPath, timeoutMs = DEFAULT_ if (env.IMPECCABLE_LIVE_COPY_AGENT_MODEL) { args.push('--model', env.IMPECCABLE_LIVE_COPY_AGENT_MODEL); } - args.push(prompt); // Forward env as-is so CLAUDE_CODE_OAUTH_TOKEN and ANTHROPIC_API_KEY flow // through. On macOS, `claude /login` stores creds in the Keychain, which a // non-TTY subprocess cannot read; setting CLAUDE_CODE_OAUTH_TOKEN (via // `claude setup-token`) is the supported headless auth path. - return runAgentProcess('claude', args, '', { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); + return runAgentProcess('claude', args, prompt, { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); } function runAgentProcess(command, args, stdin, { cwd, env, logPath, timeoutMs, mirrorOutputPath }) { diff --git a/.kiro/skills/impeccable/scripts/live-copy-edit-agent.mjs b/.kiro/skills/impeccable/scripts/live-copy-edit-agent.mjs index 313ed7f10..febc8456d 100644 --- a/.kiro/skills/impeccable/scripts/live-copy-edit-agent.mjs +++ b/.kiro/skills/impeccable/scripts/live-copy-edit-agent.mjs @@ -14,10 +14,12 @@ import path from 'node:path'; import { createRequire } from 'node:module'; const DEFAULT_TIMEOUT_MS = 60_000; +const BATCH_OP_TEXT_LIMIT = 240; const require = createRequire(import.meta.url); export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { - const repairLines = batch?.repair ? [ + const compactBatch = compactBatchForPrompt(batch); + const repairLines = compactBatch.repair ? [ '', 'Repair mode:', '- The previous Apply attempt changed source, but validation failed.', @@ -28,7 +30,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { '- If failures or candidates show edited text is also a lookup key, update coupled count, animation, icon, image, asset, style, or metadata keys in the current source, or fail that entry without partial edits.', '- Keep failed and notes as arrays.', '- Return the same canonical JSON shape after repair.', - JSON.stringify(batch.repair, null, 2), + JSON.stringify(compactBatch.repair, null, 2), ] : []; return [ 'You are the Impeccable staged copy-edit batch applier.', @@ -80,7 +82,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { ...repairLines, '', 'Staged copy-edit batch:', - JSON.stringify(compactBatchForPrompt(batch), null, 2), + JSON.stringify(compactBatch, null, 2), ].join('\n'); } @@ -292,7 +294,7 @@ function readManualEditValidationScript(cwd) { function compactBatchForPrompt(batch) { return { pageUrl: batch?.pageUrl || null, - repair: batch?.repair || undefined, + repair: compactBatchRepair(batch?.repair), entries: (batch?.entries || []).map((entry) => ({ id: entry.id, pageUrl: entry.pageUrl, @@ -300,7 +302,71 @@ function compactBatchForPrompt(batch) { element: compactContextForBatch(entry.element), ops: (entry.ops || []).map(compactBatchOp), })), - candidates: batch?.candidates || [], + candidates: compactBatchCandidates(batch?.candidates), + }; +} + +function compactBatchRepair(repair) { + if (!repair || typeof repair !== 'object') return undefined; + return { + status: compactBatchString(repair.status), + attempt: normalizeOptionalBatchNumber(repair.attempt), + attempts: normalizeOptionalBatchNumber(repair.attempts), + maxAttempts: normalizeOptionalBatchNumber(repair.maxAttempts), + reason: compactBatchString(repair.reason), + transactionId: compactBatchString(repair.transactionId), + pageUrl: compactBatchString(repair.pageUrl), + failures: compactBatchDiagnostics(repair.failures), + files: compactBatchStringList(repair.files, 20), + }; +} + +function compactBatchDiagnostics(items, depth = 0) { + if (!Array.isArray(items)) return undefined; + return items.slice(0, 12).map((item) => ({ + entryId: compactBatchString(item?.entryId || item?.id), + reason: compactBatchString(item?.reason || item?.kind), + detail: compactBatchString(item?.detail), + message: compactBatchString(item?.message), + file: compactBatchString(item?.file || item?.relativeFile), + line: normalizeOptionalBatchNumber(item?.line), + ref: compactBatchString(item?.ref), + marker: compactBatchString(item?.marker), + files: compactBatchStringList(item?.files, 8), + candidates: depth < 2 ? compactBatchSourceMatches(item?.candidates, 8) : undefined, + failures: depth < 2 ? compactBatchDiagnostics(item?.failures, depth + 1) : undefined, + checks: depth < 2 ? compactBatchDiagnostics(item?.checks, depth + 1) : undefined, + })); +} + +function compactBatchCandidates(candidates) { + return (Array.isArray(candidates) ? candidates : []) + .slice(0, 24) + .map((candidate) => ({ + entryId: compactBatchString(candidate?.entryId), + ref: compactBatchString(candidate?.ref), + sourceHint: compactBatchSourceMatch(candidate?.sourceHint), + textMatches: compactBatchSourceMatches(candidate?.textMatches, 8), + objectKeyMatches: compactBatchSourceMatches(candidate?.objectKeyMatches, 8), + contextTextMatches: compactBatchSourceMatches(candidate?.contextTextMatches, 8), + locatorMatches: compactBatchSourceMatches(candidate?.locatorMatches, 6), + })); +} + +function compactBatchSourceMatches(matches, limit) { + if (!Array.isArray(matches)) return undefined; + return matches.slice(0, limit).map(compactBatchSourceMatch).filter(Boolean); +} + +function compactBatchSourceMatch(match) { + if (!match || typeof match !== 'object') return null; + return { + file: compactBatchString(match.relativeFile || match.file), + line: normalizeBatchNumber(match.line), + column: normalizeBatchNumber(match.column), + kind: compactBatchString(match.kind), + reason: compactBatchString(match.reason || match.kind), + status: compactBatchString(match.status), }; } @@ -311,25 +377,77 @@ function compactBatchOp(op) { contextRef: op.contextRef, tag: op.tag, elementId: op.elementId, - classes: op.classes, + classes: compactBatchStringList(op.classes, 24), originalText: op.originalText, newText: op.newText, deleted: op.deleted === true || undefined, - sourceHint: op.sourceHint, + sourceHint: normalizeBatchSourceHint(op.sourceHint), leaf: compactContextForBatch(op.leaf), - nearbyEditableTexts: Array.isArray(op.nearbyEditableTexts) ? op.nearbyEditableTexts.slice(0, 8) : [], + nearbyEditableTexts: compactNearbyBatchTexts(op.nearbyEditableTexts), container: compactContextForBatch(op.container), - contextHints: Array.isArray(op.contextHints) ? op.contextHints.slice(0, 12) : [], + contextHints: compactBatchStringList(op.contextHints, 12), }; } +function normalizeBatchSourceHint(hint) { + if (!hint || typeof hint !== 'object') return null; + let line = normalizeBatchNumber(hint.line); + let column = normalizeBatchNumber(hint.column); + if ((line === null || column === null) && typeof hint.loc === 'string') { + const match = hint.loc.match(/^(\d+)(?::(\d+))?/); + if (match) { + line = Number(match[1]); + if (match[2]) column = Number(match[2]); + } + } + return { + file: compactBatchString(hint.file) || '', + loc: compactBatchString(hint.loc) || '', + line, + column, + }; +} + +function normalizeBatchNumber(value) { + if (value === null || value === undefined || value === '') return null; + const number = Number(value); + return Number.isFinite(number) ? number : null; +} + +function normalizeOptionalBatchNumber(value) { + const number = normalizeBatchNumber(value); + return number === null ? undefined : number; +} + +function compactNearbyBatchTexts(items) { + return (Array.isArray(items) ? items : []) + .slice(0, 8) + .map((item) => typeof item === 'string' ? { text: truncate(item, BATCH_OP_TEXT_LIMIT) } : { + ref: compactBatchString(item?.ref), + tag: compactBatchString(item?.tag), + classes: compactBatchStringList(item?.classes, 24), + text: compactBatchString(item?.text), + }); +} + +function compactBatchStringList(items, limit) { + return (Array.isArray(items) ? items : []) + .slice(0, limit) + .filter((item) => typeof item === 'string') + .map((item) => truncate(item, BATCH_OP_TEXT_LIMIT)); +} + +function compactBatchString(value) { + return typeof value === 'string' ? truncate(value, BATCH_OP_TEXT_LIMIT) : undefined; +} + function compactContextForBatch(value) { if (!value || typeof value !== 'object') return value || null; return { - ref: value.ref, - tagName: value.tagName, - id: value.id, - classes: value.classes, + ref: compactBatchString(value.ref), + tagName: compactBatchString(value.tagName), + id: compactBatchString(value.id), + classes: compactBatchStringList(value.classes, 24), textContent: truncate(value.textContent, 900), outerHTML: truncate(stripLiveRuntimeHtml(value.outerHTML), 1800), }; @@ -470,12 +588,11 @@ function runClaude(prompt, { cwd, env, resultPath, logPath, timeoutMs = DEFAULT_ if (env.IMPECCABLE_LIVE_COPY_AGENT_MODEL) { args.push('--model', env.IMPECCABLE_LIVE_COPY_AGENT_MODEL); } - args.push(prompt); // Forward env as-is so CLAUDE_CODE_OAUTH_TOKEN and ANTHROPIC_API_KEY flow // through. On macOS, `claude /login` stores creds in the Keychain, which a // non-TTY subprocess cannot read; setting CLAUDE_CODE_OAUTH_TOKEN (via // `claude setup-token`) is the supported headless auth path. - return runAgentProcess('claude', args, '', { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); + return runAgentProcess('claude', args, prompt, { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); } function runAgentProcess(command, args, stdin, { cwd, env, logPath, timeoutMs, mirrorOutputPath }) { diff --git a/.opencode/skills/impeccable/scripts/live-copy-edit-agent.mjs b/.opencode/skills/impeccable/scripts/live-copy-edit-agent.mjs index 313ed7f10..febc8456d 100644 --- a/.opencode/skills/impeccable/scripts/live-copy-edit-agent.mjs +++ b/.opencode/skills/impeccable/scripts/live-copy-edit-agent.mjs @@ -14,10 +14,12 @@ import path from 'node:path'; import { createRequire } from 'node:module'; const DEFAULT_TIMEOUT_MS = 60_000; +const BATCH_OP_TEXT_LIMIT = 240; const require = createRequire(import.meta.url); export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { - const repairLines = batch?.repair ? [ + const compactBatch = compactBatchForPrompt(batch); + const repairLines = compactBatch.repair ? [ '', 'Repair mode:', '- The previous Apply attempt changed source, but validation failed.', @@ -28,7 +30,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { '- If failures or candidates show edited text is also a lookup key, update coupled count, animation, icon, image, asset, style, or metadata keys in the current source, or fail that entry without partial edits.', '- Keep failed and notes as arrays.', '- Return the same canonical JSON shape after repair.', - JSON.stringify(batch.repair, null, 2), + JSON.stringify(compactBatch.repair, null, 2), ] : []; return [ 'You are the Impeccable staged copy-edit batch applier.', @@ -80,7 +82,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { ...repairLines, '', 'Staged copy-edit batch:', - JSON.stringify(compactBatchForPrompt(batch), null, 2), + JSON.stringify(compactBatch, null, 2), ].join('\n'); } @@ -292,7 +294,7 @@ function readManualEditValidationScript(cwd) { function compactBatchForPrompt(batch) { return { pageUrl: batch?.pageUrl || null, - repair: batch?.repair || undefined, + repair: compactBatchRepair(batch?.repair), entries: (batch?.entries || []).map((entry) => ({ id: entry.id, pageUrl: entry.pageUrl, @@ -300,7 +302,71 @@ function compactBatchForPrompt(batch) { element: compactContextForBatch(entry.element), ops: (entry.ops || []).map(compactBatchOp), })), - candidates: batch?.candidates || [], + candidates: compactBatchCandidates(batch?.candidates), + }; +} + +function compactBatchRepair(repair) { + if (!repair || typeof repair !== 'object') return undefined; + return { + status: compactBatchString(repair.status), + attempt: normalizeOptionalBatchNumber(repair.attempt), + attempts: normalizeOptionalBatchNumber(repair.attempts), + maxAttempts: normalizeOptionalBatchNumber(repair.maxAttempts), + reason: compactBatchString(repair.reason), + transactionId: compactBatchString(repair.transactionId), + pageUrl: compactBatchString(repair.pageUrl), + failures: compactBatchDiagnostics(repair.failures), + files: compactBatchStringList(repair.files, 20), + }; +} + +function compactBatchDiagnostics(items, depth = 0) { + if (!Array.isArray(items)) return undefined; + return items.slice(0, 12).map((item) => ({ + entryId: compactBatchString(item?.entryId || item?.id), + reason: compactBatchString(item?.reason || item?.kind), + detail: compactBatchString(item?.detail), + message: compactBatchString(item?.message), + file: compactBatchString(item?.file || item?.relativeFile), + line: normalizeOptionalBatchNumber(item?.line), + ref: compactBatchString(item?.ref), + marker: compactBatchString(item?.marker), + files: compactBatchStringList(item?.files, 8), + candidates: depth < 2 ? compactBatchSourceMatches(item?.candidates, 8) : undefined, + failures: depth < 2 ? compactBatchDiagnostics(item?.failures, depth + 1) : undefined, + checks: depth < 2 ? compactBatchDiagnostics(item?.checks, depth + 1) : undefined, + })); +} + +function compactBatchCandidates(candidates) { + return (Array.isArray(candidates) ? candidates : []) + .slice(0, 24) + .map((candidate) => ({ + entryId: compactBatchString(candidate?.entryId), + ref: compactBatchString(candidate?.ref), + sourceHint: compactBatchSourceMatch(candidate?.sourceHint), + textMatches: compactBatchSourceMatches(candidate?.textMatches, 8), + objectKeyMatches: compactBatchSourceMatches(candidate?.objectKeyMatches, 8), + contextTextMatches: compactBatchSourceMatches(candidate?.contextTextMatches, 8), + locatorMatches: compactBatchSourceMatches(candidate?.locatorMatches, 6), + })); +} + +function compactBatchSourceMatches(matches, limit) { + if (!Array.isArray(matches)) return undefined; + return matches.slice(0, limit).map(compactBatchSourceMatch).filter(Boolean); +} + +function compactBatchSourceMatch(match) { + if (!match || typeof match !== 'object') return null; + return { + file: compactBatchString(match.relativeFile || match.file), + line: normalizeBatchNumber(match.line), + column: normalizeBatchNumber(match.column), + kind: compactBatchString(match.kind), + reason: compactBatchString(match.reason || match.kind), + status: compactBatchString(match.status), }; } @@ -311,25 +377,77 @@ function compactBatchOp(op) { contextRef: op.contextRef, tag: op.tag, elementId: op.elementId, - classes: op.classes, + classes: compactBatchStringList(op.classes, 24), originalText: op.originalText, newText: op.newText, deleted: op.deleted === true || undefined, - sourceHint: op.sourceHint, + sourceHint: normalizeBatchSourceHint(op.sourceHint), leaf: compactContextForBatch(op.leaf), - nearbyEditableTexts: Array.isArray(op.nearbyEditableTexts) ? op.nearbyEditableTexts.slice(0, 8) : [], + nearbyEditableTexts: compactNearbyBatchTexts(op.nearbyEditableTexts), container: compactContextForBatch(op.container), - contextHints: Array.isArray(op.contextHints) ? op.contextHints.slice(0, 12) : [], + contextHints: compactBatchStringList(op.contextHints, 12), }; } +function normalizeBatchSourceHint(hint) { + if (!hint || typeof hint !== 'object') return null; + let line = normalizeBatchNumber(hint.line); + let column = normalizeBatchNumber(hint.column); + if ((line === null || column === null) && typeof hint.loc === 'string') { + const match = hint.loc.match(/^(\d+)(?::(\d+))?/); + if (match) { + line = Number(match[1]); + if (match[2]) column = Number(match[2]); + } + } + return { + file: compactBatchString(hint.file) || '', + loc: compactBatchString(hint.loc) || '', + line, + column, + }; +} + +function normalizeBatchNumber(value) { + if (value === null || value === undefined || value === '') return null; + const number = Number(value); + return Number.isFinite(number) ? number : null; +} + +function normalizeOptionalBatchNumber(value) { + const number = normalizeBatchNumber(value); + return number === null ? undefined : number; +} + +function compactNearbyBatchTexts(items) { + return (Array.isArray(items) ? items : []) + .slice(0, 8) + .map((item) => typeof item === 'string' ? { text: truncate(item, BATCH_OP_TEXT_LIMIT) } : { + ref: compactBatchString(item?.ref), + tag: compactBatchString(item?.tag), + classes: compactBatchStringList(item?.classes, 24), + text: compactBatchString(item?.text), + }); +} + +function compactBatchStringList(items, limit) { + return (Array.isArray(items) ? items : []) + .slice(0, limit) + .filter((item) => typeof item === 'string') + .map((item) => truncate(item, BATCH_OP_TEXT_LIMIT)); +} + +function compactBatchString(value) { + return typeof value === 'string' ? truncate(value, BATCH_OP_TEXT_LIMIT) : undefined; +} + function compactContextForBatch(value) { if (!value || typeof value !== 'object') return value || null; return { - ref: value.ref, - tagName: value.tagName, - id: value.id, - classes: value.classes, + ref: compactBatchString(value.ref), + tagName: compactBatchString(value.tagName), + id: compactBatchString(value.id), + classes: compactBatchStringList(value.classes, 24), textContent: truncate(value.textContent, 900), outerHTML: truncate(stripLiveRuntimeHtml(value.outerHTML), 1800), }; @@ -470,12 +588,11 @@ function runClaude(prompt, { cwd, env, resultPath, logPath, timeoutMs = DEFAULT_ if (env.IMPECCABLE_LIVE_COPY_AGENT_MODEL) { args.push('--model', env.IMPECCABLE_LIVE_COPY_AGENT_MODEL); } - args.push(prompt); // Forward env as-is so CLAUDE_CODE_OAUTH_TOKEN and ANTHROPIC_API_KEY flow // through. On macOS, `claude /login` stores creds in the Keychain, which a // non-TTY subprocess cannot read; setting CLAUDE_CODE_OAUTH_TOKEN (via // `claude setup-token`) is the supported headless auth path. - return runAgentProcess('claude', args, '', { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); + return runAgentProcess('claude', args, prompt, { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); } function runAgentProcess(command, args, stdin, { cwd, env, logPath, timeoutMs, mirrorOutputPath }) { diff --git a/.pi/skills/impeccable/scripts/live-copy-edit-agent.mjs b/.pi/skills/impeccable/scripts/live-copy-edit-agent.mjs index 313ed7f10..febc8456d 100644 --- a/.pi/skills/impeccable/scripts/live-copy-edit-agent.mjs +++ b/.pi/skills/impeccable/scripts/live-copy-edit-agent.mjs @@ -14,10 +14,12 @@ import path from 'node:path'; import { createRequire } from 'node:module'; const DEFAULT_TIMEOUT_MS = 60_000; +const BATCH_OP_TEXT_LIMIT = 240; const require = createRequire(import.meta.url); export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { - const repairLines = batch?.repair ? [ + const compactBatch = compactBatchForPrompt(batch); + const repairLines = compactBatch.repair ? [ '', 'Repair mode:', '- The previous Apply attempt changed source, but validation failed.', @@ -28,7 +30,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { '- If failures or candidates show edited text is also a lookup key, update coupled count, animation, icon, image, asset, style, or metadata keys in the current source, or fail that entry without partial edits.', '- Keep failed and notes as arrays.', '- Return the same canonical JSON shape after repair.', - JSON.stringify(batch.repair, null, 2), + JSON.stringify(compactBatch.repair, null, 2), ] : []; return [ 'You are the Impeccable staged copy-edit batch applier.', @@ -80,7 +82,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { ...repairLines, '', 'Staged copy-edit batch:', - JSON.stringify(compactBatchForPrompt(batch), null, 2), + JSON.stringify(compactBatch, null, 2), ].join('\n'); } @@ -292,7 +294,7 @@ function readManualEditValidationScript(cwd) { function compactBatchForPrompt(batch) { return { pageUrl: batch?.pageUrl || null, - repair: batch?.repair || undefined, + repair: compactBatchRepair(batch?.repair), entries: (batch?.entries || []).map((entry) => ({ id: entry.id, pageUrl: entry.pageUrl, @@ -300,7 +302,71 @@ function compactBatchForPrompt(batch) { element: compactContextForBatch(entry.element), ops: (entry.ops || []).map(compactBatchOp), })), - candidates: batch?.candidates || [], + candidates: compactBatchCandidates(batch?.candidates), + }; +} + +function compactBatchRepair(repair) { + if (!repair || typeof repair !== 'object') return undefined; + return { + status: compactBatchString(repair.status), + attempt: normalizeOptionalBatchNumber(repair.attempt), + attempts: normalizeOptionalBatchNumber(repair.attempts), + maxAttempts: normalizeOptionalBatchNumber(repair.maxAttempts), + reason: compactBatchString(repair.reason), + transactionId: compactBatchString(repair.transactionId), + pageUrl: compactBatchString(repair.pageUrl), + failures: compactBatchDiagnostics(repair.failures), + files: compactBatchStringList(repair.files, 20), + }; +} + +function compactBatchDiagnostics(items, depth = 0) { + if (!Array.isArray(items)) return undefined; + return items.slice(0, 12).map((item) => ({ + entryId: compactBatchString(item?.entryId || item?.id), + reason: compactBatchString(item?.reason || item?.kind), + detail: compactBatchString(item?.detail), + message: compactBatchString(item?.message), + file: compactBatchString(item?.file || item?.relativeFile), + line: normalizeOptionalBatchNumber(item?.line), + ref: compactBatchString(item?.ref), + marker: compactBatchString(item?.marker), + files: compactBatchStringList(item?.files, 8), + candidates: depth < 2 ? compactBatchSourceMatches(item?.candidates, 8) : undefined, + failures: depth < 2 ? compactBatchDiagnostics(item?.failures, depth + 1) : undefined, + checks: depth < 2 ? compactBatchDiagnostics(item?.checks, depth + 1) : undefined, + })); +} + +function compactBatchCandidates(candidates) { + return (Array.isArray(candidates) ? candidates : []) + .slice(0, 24) + .map((candidate) => ({ + entryId: compactBatchString(candidate?.entryId), + ref: compactBatchString(candidate?.ref), + sourceHint: compactBatchSourceMatch(candidate?.sourceHint), + textMatches: compactBatchSourceMatches(candidate?.textMatches, 8), + objectKeyMatches: compactBatchSourceMatches(candidate?.objectKeyMatches, 8), + contextTextMatches: compactBatchSourceMatches(candidate?.contextTextMatches, 8), + locatorMatches: compactBatchSourceMatches(candidate?.locatorMatches, 6), + })); +} + +function compactBatchSourceMatches(matches, limit) { + if (!Array.isArray(matches)) return undefined; + return matches.slice(0, limit).map(compactBatchSourceMatch).filter(Boolean); +} + +function compactBatchSourceMatch(match) { + if (!match || typeof match !== 'object') return null; + return { + file: compactBatchString(match.relativeFile || match.file), + line: normalizeBatchNumber(match.line), + column: normalizeBatchNumber(match.column), + kind: compactBatchString(match.kind), + reason: compactBatchString(match.reason || match.kind), + status: compactBatchString(match.status), }; } @@ -311,25 +377,77 @@ function compactBatchOp(op) { contextRef: op.contextRef, tag: op.tag, elementId: op.elementId, - classes: op.classes, + classes: compactBatchStringList(op.classes, 24), originalText: op.originalText, newText: op.newText, deleted: op.deleted === true || undefined, - sourceHint: op.sourceHint, + sourceHint: normalizeBatchSourceHint(op.sourceHint), leaf: compactContextForBatch(op.leaf), - nearbyEditableTexts: Array.isArray(op.nearbyEditableTexts) ? op.nearbyEditableTexts.slice(0, 8) : [], + nearbyEditableTexts: compactNearbyBatchTexts(op.nearbyEditableTexts), container: compactContextForBatch(op.container), - contextHints: Array.isArray(op.contextHints) ? op.contextHints.slice(0, 12) : [], + contextHints: compactBatchStringList(op.contextHints, 12), }; } +function normalizeBatchSourceHint(hint) { + if (!hint || typeof hint !== 'object') return null; + let line = normalizeBatchNumber(hint.line); + let column = normalizeBatchNumber(hint.column); + if ((line === null || column === null) && typeof hint.loc === 'string') { + const match = hint.loc.match(/^(\d+)(?::(\d+))?/); + if (match) { + line = Number(match[1]); + if (match[2]) column = Number(match[2]); + } + } + return { + file: compactBatchString(hint.file) || '', + loc: compactBatchString(hint.loc) || '', + line, + column, + }; +} + +function normalizeBatchNumber(value) { + if (value === null || value === undefined || value === '') return null; + const number = Number(value); + return Number.isFinite(number) ? number : null; +} + +function normalizeOptionalBatchNumber(value) { + const number = normalizeBatchNumber(value); + return number === null ? undefined : number; +} + +function compactNearbyBatchTexts(items) { + return (Array.isArray(items) ? items : []) + .slice(0, 8) + .map((item) => typeof item === 'string' ? { text: truncate(item, BATCH_OP_TEXT_LIMIT) } : { + ref: compactBatchString(item?.ref), + tag: compactBatchString(item?.tag), + classes: compactBatchStringList(item?.classes, 24), + text: compactBatchString(item?.text), + }); +} + +function compactBatchStringList(items, limit) { + return (Array.isArray(items) ? items : []) + .slice(0, limit) + .filter((item) => typeof item === 'string') + .map((item) => truncate(item, BATCH_OP_TEXT_LIMIT)); +} + +function compactBatchString(value) { + return typeof value === 'string' ? truncate(value, BATCH_OP_TEXT_LIMIT) : undefined; +} + function compactContextForBatch(value) { if (!value || typeof value !== 'object') return value || null; return { - ref: value.ref, - tagName: value.tagName, - id: value.id, - classes: value.classes, + ref: compactBatchString(value.ref), + tagName: compactBatchString(value.tagName), + id: compactBatchString(value.id), + classes: compactBatchStringList(value.classes, 24), textContent: truncate(value.textContent, 900), outerHTML: truncate(stripLiveRuntimeHtml(value.outerHTML), 1800), }; @@ -470,12 +588,11 @@ function runClaude(prompt, { cwd, env, resultPath, logPath, timeoutMs = DEFAULT_ if (env.IMPECCABLE_LIVE_COPY_AGENT_MODEL) { args.push('--model', env.IMPECCABLE_LIVE_COPY_AGENT_MODEL); } - args.push(prompt); // Forward env as-is so CLAUDE_CODE_OAUTH_TOKEN and ANTHROPIC_API_KEY flow // through. On macOS, `claude /login` stores creds in the Keychain, which a // non-TTY subprocess cannot read; setting CLAUDE_CODE_OAUTH_TOKEN (via // `claude setup-token`) is the supported headless auth path. - return runAgentProcess('claude', args, '', { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); + return runAgentProcess('claude', args, prompt, { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); } function runAgentProcess(command, args, stdin, { cwd, env, logPath, timeoutMs, mirrorOutputPath }) { diff --git a/.qoder/skills/impeccable/scripts/live-copy-edit-agent.mjs b/.qoder/skills/impeccable/scripts/live-copy-edit-agent.mjs index 313ed7f10..febc8456d 100644 --- a/.qoder/skills/impeccable/scripts/live-copy-edit-agent.mjs +++ b/.qoder/skills/impeccable/scripts/live-copy-edit-agent.mjs @@ -14,10 +14,12 @@ import path from 'node:path'; import { createRequire } from 'node:module'; const DEFAULT_TIMEOUT_MS = 60_000; +const BATCH_OP_TEXT_LIMIT = 240; const require = createRequire(import.meta.url); export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { - const repairLines = batch?.repair ? [ + const compactBatch = compactBatchForPrompt(batch); + const repairLines = compactBatch.repair ? [ '', 'Repair mode:', '- The previous Apply attempt changed source, but validation failed.', @@ -28,7 +30,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { '- If failures or candidates show edited text is also a lookup key, update coupled count, animation, icon, image, asset, style, or metadata keys in the current source, or fail that entry without partial edits.', '- Keep failed and notes as arrays.', '- Return the same canonical JSON shape after repair.', - JSON.stringify(batch.repair, null, 2), + JSON.stringify(compactBatch.repair, null, 2), ] : []; return [ 'You are the Impeccable staged copy-edit batch applier.', @@ -80,7 +82,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { ...repairLines, '', 'Staged copy-edit batch:', - JSON.stringify(compactBatchForPrompt(batch), null, 2), + JSON.stringify(compactBatch, null, 2), ].join('\n'); } @@ -292,7 +294,7 @@ function readManualEditValidationScript(cwd) { function compactBatchForPrompt(batch) { return { pageUrl: batch?.pageUrl || null, - repair: batch?.repair || undefined, + repair: compactBatchRepair(batch?.repair), entries: (batch?.entries || []).map((entry) => ({ id: entry.id, pageUrl: entry.pageUrl, @@ -300,7 +302,71 @@ function compactBatchForPrompt(batch) { element: compactContextForBatch(entry.element), ops: (entry.ops || []).map(compactBatchOp), })), - candidates: batch?.candidates || [], + candidates: compactBatchCandidates(batch?.candidates), + }; +} + +function compactBatchRepair(repair) { + if (!repair || typeof repair !== 'object') return undefined; + return { + status: compactBatchString(repair.status), + attempt: normalizeOptionalBatchNumber(repair.attempt), + attempts: normalizeOptionalBatchNumber(repair.attempts), + maxAttempts: normalizeOptionalBatchNumber(repair.maxAttempts), + reason: compactBatchString(repair.reason), + transactionId: compactBatchString(repair.transactionId), + pageUrl: compactBatchString(repair.pageUrl), + failures: compactBatchDiagnostics(repair.failures), + files: compactBatchStringList(repair.files, 20), + }; +} + +function compactBatchDiagnostics(items, depth = 0) { + if (!Array.isArray(items)) return undefined; + return items.slice(0, 12).map((item) => ({ + entryId: compactBatchString(item?.entryId || item?.id), + reason: compactBatchString(item?.reason || item?.kind), + detail: compactBatchString(item?.detail), + message: compactBatchString(item?.message), + file: compactBatchString(item?.file || item?.relativeFile), + line: normalizeOptionalBatchNumber(item?.line), + ref: compactBatchString(item?.ref), + marker: compactBatchString(item?.marker), + files: compactBatchStringList(item?.files, 8), + candidates: depth < 2 ? compactBatchSourceMatches(item?.candidates, 8) : undefined, + failures: depth < 2 ? compactBatchDiagnostics(item?.failures, depth + 1) : undefined, + checks: depth < 2 ? compactBatchDiagnostics(item?.checks, depth + 1) : undefined, + })); +} + +function compactBatchCandidates(candidates) { + return (Array.isArray(candidates) ? candidates : []) + .slice(0, 24) + .map((candidate) => ({ + entryId: compactBatchString(candidate?.entryId), + ref: compactBatchString(candidate?.ref), + sourceHint: compactBatchSourceMatch(candidate?.sourceHint), + textMatches: compactBatchSourceMatches(candidate?.textMatches, 8), + objectKeyMatches: compactBatchSourceMatches(candidate?.objectKeyMatches, 8), + contextTextMatches: compactBatchSourceMatches(candidate?.contextTextMatches, 8), + locatorMatches: compactBatchSourceMatches(candidate?.locatorMatches, 6), + })); +} + +function compactBatchSourceMatches(matches, limit) { + if (!Array.isArray(matches)) return undefined; + return matches.slice(0, limit).map(compactBatchSourceMatch).filter(Boolean); +} + +function compactBatchSourceMatch(match) { + if (!match || typeof match !== 'object') return null; + return { + file: compactBatchString(match.relativeFile || match.file), + line: normalizeBatchNumber(match.line), + column: normalizeBatchNumber(match.column), + kind: compactBatchString(match.kind), + reason: compactBatchString(match.reason || match.kind), + status: compactBatchString(match.status), }; } @@ -311,25 +377,77 @@ function compactBatchOp(op) { contextRef: op.contextRef, tag: op.tag, elementId: op.elementId, - classes: op.classes, + classes: compactBatchStringList(op.classes, 24), originalText: op.originalText, newText: op.newText, deleted: op.deleted === true || undefined, - sourceHint: op.sourceHint, + sourceHint: normalizeBatchSourceHint(op.sourceHint), leaf: compactContextForBatch(op.leaf), - nearbyEditableTexts: Array.isArray(op.nearbyEditableTexts) ? op.nearbyEditableTexts.slice(0, 8) : [], + nearbyEditableTexts: compactNearbyBatchTexts(op.nearbyEditableTexts), container: compactContextForBatch(op.container), - contextHints: Array.isArray(op.contextHints) ? op.contextHints.slice(0, 12) : [], + contextHints: compactBatchStringList(op.contextHints, 12), }; } +function normalizeBatchSourceHint(hint) { + if (!hint || typeof hint !== 'object') return null; + let line = normalizeBatchNumber(hint.line); + let column = normalizeBatchNumber(hint.column); + if ((line === null || column === null) && typeof hint.loc === 'string') { + const match = hint.loc.match(/^(\d+)(?::(\d+))?/); + if (match) { + line = Number(match[1]); + if (match[2]) column = Number(match[2]); + } + } + return { + file: compactBatchString(hint.file) || '', + loc: compactBatchString(hint.loc) || '', + line, + column, + }; +} + +function normalizeBatchNumber(value) { + if (value === null || value === undefined || value === '') return null; + const number = Number(value); + return Number.isFinite(number) ? number : null; +} + +function normalizeOptionalBatchNumber(value) { + const number = normalizeBatchNumber(value); + return number === null ? undefined : number; +} + +function compactNearbyBatchTexts(items) { + return (Array.isArray(items) ? items : []) + .slice(0, 8) + .map((item) => typeof item === 'string' ? { text: truncate(item, BATCH_OP_TEXT_LIMIT) } : { + ref: compactBatchString(item?.ref), + tag: compactBatchString(item?.tag), + classes: compactBatchStringList(item?.classes, 24), + text: compactBatchString(item?.text), + }); +} + +function compactBatchStringList(items, limit) { + return (Array.isArray(items) ? items : []) + .slice(0, limit) + .filter((item) => typeof item === 'string') + .map((item) => truncate(item, BATCH_OP_TEXT_LIMIT)); +} + +function compactBatchString(value) { + return typeof value === 'string' ? truncate(value, BATCH_OP_TEXT_LIMIT) : undefined; +} + function compactContextForBatch(value) { if (!value || typeof value !== 'object') return value || null; return { - ref: value.ref, - tagName: value.tagName, - id: value.id, - classes: value.classes, + ref: compactBatchString(value.ref), + tagName: compactBatchString(value.tagName), + id: compactBatchString(value.id), + classes: compactBatchStringList(value.classes, 24), textContent: truncate(value.textContent, 900), outerHTML: truncate(stripLiveRuntimeHtml(value.outerHTML), 1800), }; @@ -470,12 +588,11 @@ function runClaude(prompt, { cwd, env, resultPath, logPath, timeoutMs = DEFAULT_ if (env.IMPECCABLE_LIVE_COPY_AGENT_MODEL) { args.push('--model', env.IMPECCABLE_LIVE_COPY_AGENT_MODEL); } - args.push(prompt); // Forward env as-is so CLAUDE_CODE_OAUTH_TOKEN and ANTHROPIC_API_KEY flow // through. On macOS, `claude /login` stores creds in the Keychain, which a // non-TTY subprocess cannot read; setting CLAUDE_CODE_OAUTH_TOKEN (via // `claude setup-token`) is the supported headless auth path. - return runAgentProcess('claude', args, '', { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); + return runAgentProcess('claude', args, prompt, { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); } function runAgentProcess(command, args, stdin, { cwd, env, logPath, timeoutMs, mirrorOutputPath }) { diff --git a/.rovodev/skills/impeccable/scripts/live-copy-edit-agent.mjs b/.rovodev/skills/impeccable/scripts/live-copy-edit-agent.mjs index 313ed7f10..febc8456d 100644 --- a/.rovodev/skills/impeccable/scripts/live-copy-edit-agent.mjs +++ b/.rovodev/skills/impeccable/scripts/live-copy-edit-agent.mjs @@ -14,10 +14,12 @@ import path from 'node:path'; import { createRequire } from 'node:module'; const DEFAULT_TIMEOUT_MS = 60_000; +const BATCH_OP_TEXT_LIMIT = 240; const require = createRequire(import.meta.url); export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { - const repairLines = batch?.repair ? [ + const compactBatch = compactBatchForPrompt(batch); + const repairLines = compactBatch.repair ? [ '', 'Repair mode:', '- The previous Apply attempt changed source, but validation failed.', @@ -28,7 +30,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { '- If failures or candidates show edited text is also a lookup key, update coupled count, animation, icon, image, asset, style, or metadata keys in the current source, or fail that entry without partial edits.', '- Keep failed and notes as arrays.', '- Return the same canonical JSON shape after repair.', - JSON.stringify(batch.repair, null, 2), + JSON.stringify(compactBatch.repair, null, 2), ] : []; return [ 'You are the Impeccable staged copy-edit batch applier.', @@ -80,7 +82,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { ...repairLines, '', 'Staged copy-edit batch:', - JSON.stringify(compactBatchForPrompt(batch), null, 2), + JSON.stringify(compactBatch, null, 2), ].join('\n'); } @@ -292,7 +294,7 @@ function readManualEditValidationScript(cwd) { function compactBatchForPrompt(batch) { return { pageUrl: batch?.pageUrl || null, - repair: batch?.repair || undefined, + repair: compactBatchRepair(batch?.repair), entries: (batch?.entries || []).map((entry) => ({ id: entry.id, pageUrl: entry.pageUrl, @@ -300,7 +302,71 @@ function compactBatchForPrompt(batch) { element: compactContextForBatch(entry.element), ops: (entry.ops || []).map(compactBatchOp), })), - candidates: batch?.candidates || [], + candidates: compactBatchCandidates(batch?.candidates), + }; +} + +function compactBatchRepair(repair) { + if (!repair || typeof repair !== 'object') return undefined; + return { + status: compactBatchString(repair.status), + attempt: normalizeOptionalBatchNumber(repair.attempt), + attempts: normalizeOptionalBatchNumber(repair.attempts), + maxAttempts: normalizeOptionalBatchNumber(repair.maxAttempts), + reason: compactBatchString(repair.reason), + transactionId: compactBatchString(repair.transactionId), + pageUrl: compactBatchString(repair.pageUrl), + failures: compactBatchDiagnostics(repair.failures), + files: compactBatchStringList(repair.files, 20), + }; +} + +function compactBatchDiagnostics(items, depth = 0) { + if (!Array.isArray(items)) return undefined; + return items.slice(0, 12).map((item) => ({ + entryId: compactBatchString(item?.entryId || item?.id), + reason: compactBatchString(item?.reason || item?.kind), + detail: compactBatchString(item?.detail), + message: compactBatchString(item?.message), + file: compactBatchString(item?.file || item?.relativeFile), + line: normalizeOptionalBatchNumber(item?.line), + ref: compactBatchString(item?.ref), + marker: compactBatchString(item?.marker), + files: compactBatchStringList(item?.files, 8), + candidates: depth < 2 ? compactBatchSourceMatches(item?.candidates, 8) : undefined, + failures: depth < 2 ? compactBatchDiagnostics(item?.failures, depth + 1) : undefined, + checks: depth < 2 ? compactBatchDiagnostics(item?.checks, depth + 1) : undefined, + })); +} + +function compactBatchCandidates(candidates) { + return (Array.isArray(candidates) ? candidates : []) + .slice(0, 24) + .map((candidate) => ({ + entryId: compactBatchString(candidate?.entryId), + ref: compactBatchString(candidate?.ref), + sourceHint: compactBatchSourceMatch(candidate?.sourceHint), + textMatches: compactBatchSourceMatches(candidate?.textMatches, 8), + objectKeyMatches: compactBatchSourceMatches(candidate?.objectKeyMatches, 8), + contextTextMatches: compactBatchSourceMatches(candidate?.contextTextMatches, 8), + locatorMatches: compactBatchSourceMatches(candidate?.locatorMatches, 6), + })); +} + +function compactBatchSourceMatches(matches, limit) { + if (!Array.isArray(matches)) return undefined; + return matches.slice(0, limit).map(compactBatchSourceMatch).filter(Boolean); +} + +function compactBatchSourceMatch(match) { + if (!match || typeof match !== 'object') return null; + return { + file: compactBatchString(match.relativeFile || match.file), + line: normalizeBatchNumber(match.line), + column: normalizeBatchNumber(match.column), + kind: compactBatchString(match.kind), + reason: compactBatchString(match.reason || match.kind), + status: compactBatchString(match.status), }; } @@ -311,25 +377,77 @@ function compactBatchOp(op) { contextRef: op.contextRef, tag: op.tag, elementId: op.elementId, - classes: op.classes, + classes: compactBatchStringList(op.classes, 24), originalText: op.originalText, newText: op.newText, deleted: op.deleted === true || undefined, - sourceHint: op.sourceHint, + sourceHint: normalizeBatchSourceHint(op.sourceHint), leaf: compactContextForBatch(op.leaf), - nearbyEditableTexts: Array.isArray(op.nearbyEditableTexts) ? op.nearbyEditableTexts.slice(0, 8) : [], + nearbyEditableTexts: compactNearbyBatchTexts(op.nearbyEditableTexts), container: compactContextForBatch(op.container), - contextHints: Array.isArray(op.contextHints) ? op.contextHints.slice(0, 12) : [], + contextHints: compactBatchStringList(op.contextHints, 12), }; } +function normalizeBatchSourceHint(hint) { + if (!hint || typeof hint !== 'object') return null; + let line = normalizeBatchNumber(hint.line); + let column = normalizeBatchNumber(hint.column); + if ((line === null || column === null) && typeof hint.loc === 'string') { + const match = hint.loc.match(/^(\d+)(?::(\d+))?/); + if (match) { + line = Number(match[1]); + if (match[2]) column = Number(match[2]); + } + } + return { + file: compactBatchString(hint.file) || '', + loc: compactBatchString(hint.loc) || '', + line, + column, + }; +} + +function normalizeBatchNumber(value) { + if (value === null || value === undefined || value === '') return null; + const number = Number(value); + return Number.isFinite(number) ? number : null; +} + +function normalizeOptionalBatchNumber(value) { + const number = normalizeBatchNumber(value); + return number === null ? undefined : number; +} + +function compactNearbyBatchTexts(items) { + return (Array.isArray(items) ? items : []) + .slice(0, 8) + .map((item) => typeof item === 'string' ? { text: truncate(item, BATCH_OP_TEXT_LIMIT) } : { + ref: compactBatchString(item?.ref), + tag: compactBatchString(item?.tag), + classes: compactBatchStringList(item?.classes, 24), + text: compactBatchString(item?.text), + }); +} + +function compactBatchStringList(items, limit) { + return (Array.isArray(items) ? items : []) + .slice(0, limit) + .filter((item) => typeof item === 'string') + .map((item) => truncate(item, BATCH_OP_TEXT_LIMIT)); +} + +function compactBatchString(value) { + return typeof value === 'string' ? truncate(value, BATCH_OP_TEXT_LIMIT) : undefined; +} + function compactContextForBatch(value) { if (!value || typeof value !== 'object') return value || null; return { - ref: value.ref, - tagName: value.tagName, - id: value.id, - classes: value.classes, + ref: compactBatchString(value.ref), + tagName: compactBatchString(value.tagName), + id: compactBatchString(value.id), + classes: compactBatchStringList(value.classes, 24), textContent: truncate(value.textContent, 900), outerHTML: truncate(stripLiveRuntimeHtml(value.outerHTML), 1800), }; @@ -470,12 +588,11 @@ function runClaude(prompt, { cwd, env, resultPath, logPath, timeoutMs = DEFAULT_ if (env.IMPECCABLE_LIVE_COPY_AGENT_MODEL) { args.push('--model', env.IMPECCABLE_LIVE_COPY_AGENT_MODEL); } - args.push(prompt); // Forward env as-is so CLAUDE_CODE_OAUTH_TOKEN and ANTHROPIC_API_KEY flow // through. On macOS, `claude /login` stores creds in the Keychain, which a // non-TTY subprocess cannot read; setting CLAUDE_CODE_OAUTH_TOKEN (via // `claude setup-token`) is the supported headless auth path. - return runAgentProcess('claude', args, '', { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); + return runAgentProcess('claude', args, prompt, { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); } function runAgentProcess(command, args, stdin, { cwd, env, logPath, timeoutMs, mirrorOutputPath }) { diff --git a/.trae-cn/skills/impeccable/scripts/live-copy-edit-agent.mjs b/.trae-cn/skills/impeccable/scripts/live-copy-edit-agent.mjs index 313ed7f10..febc8456d 100644 --- a/.trae-cn/skills/impeccable/scripts/live-copy-edit-agent.mjs +++ b/.trae-cn/skills/impeccable/scripts/live-copy-edit-agent.mjs @@ -14,10 +14,12 @@ import path from 'node:path'; import { createRequire } from 'node:module'; const DEFAULT_TIMEOUT_MS = 60_000; +const BATCH_OP_TEXT_LIMIT = 240; const require = createRequire(import.meta.url); export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { - const repairLines = batch?.repair ? [ + const compactBatch = compactBatchForPrompt(batch); + const repairLines = compactBatch.repair ? [ '', 'Repair mode:', '- The previous Apply attempt changed source, but validation failed.', @@ -28,7 +30,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { '- If failures or candidates show edited text is also a lookup key, update coupled count, animation, icon, image, asset, style, or metadata keys in the current source, or fail that entry without partial edits.', '- Keep failed and notes as arrays.', '- Return the same canonical JSON shape after repair.', - JSON.stringify(batch.repair, null, 2), + JSON.stringify(compactBatch.repair, null, 2), ] : []; return [ 'You are the Impeccable staged copy-edit batch applier.', @@ -80,7 +82,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { ...repairLines, '', 'Staged copy-edit batch:', - JSON.stringify(compactBatchForPrompt(batch), null, 2), + JSON.stringify(compactBatch, null, 2), ].join('\n'); } @@ -292,7 +294,7 @@ function readManualEditValidationScript(cwd) { function compactBatchForPrompt(batch) { return { pageUrl: batch?.pageUrl || null, - repair: batch?.repair || undefined, + repair: compactBatchRepair(batch?.repair), entries: (batch?.entries || []).map((entry) => ({ id: entry.id, pageUrl: entry.pageUrl, @@ -300,7 +302,71 @@ function compactBatchForPrompt(batch) { element: compactContextForBatch(entry.element), ops: (entry.ops || []).map(compactBatchOp), })), - candidates: batch?.candidates || [], + candidates: compactBatchCandidates(batch?.candidates), + }; +} + +function compactBatchRepair(repair) { + if (!repair || typeof repair !== 'object') return undefined; + return { + status: compactBatchString(repair.status), + attempt: normalizeOptionalBatchNumber(repair.attempt), + attempts: normalizeOptionalBatchNumber(repair.attempts), + maxAttempts: normalizeOptionalBatchNumber(repair.maxAttempts), + reason: compactBatchString(repair.reason), + transactionId: compactBatchString(repair.transactionId), + pageUrl: compactBatchString(repair.pageUrl), + failures: compactBatchDiagnostics(repair.failures), + files: compactBatchStringList(repair.files, 20), + }; +} + +function compactBatchDiagnostics(items, depth = 0) { + if (!Array.isArray(items)) return undefined; + return items.slice(0, 12).map((item) => ({ + entryId: compactBatchString(item?.entryId || item?.id), + reason: compactBatchString(item?.reason || item?.kind), + detail: compactBatchString(item?.detail), + message: compactBatchString(item?.message), + file: compactBatchString(item?.file || item?.relativeFile), + line: normalizeOptionalBatchNumber(item?.line), + ref: compactBatchString(item?.ref), + marker: compactBatchString(item?.marker), + files: compactBatchStringList(item?.files, 8), + candidates: depth < 2 ? compactBatchSourceMatches(item?.candidates, 8) : undefined, + failures: depth < 2 ? compactBatchDiagnostics(item?.failures, depth + 1) : undefined, + checks: depth < 2 ? compactBatchDiagnostics(item?.checks, depth + 1) : undefined, + })); +} + +function compactBatchCandidates(candidates) { + return (Array.isArray(candidates) ? candidates : []) + .slice(0, 24) + .map((candidate) => ({ + entryId: compactBatchString(candidate?.entryId), + ref: compactBatchString(candidate?.ref), + sourceHint: compactBatchSourceMatch(candidate?.sourceHint), + textMatches: compactBatchSourceMatches(candidate?.textMatches, 8), + objectKeyMatches: compactBatchSourceMatches(candidate?.objectKeyMatches, 8), + contextTextMatches: compactBatchSourceMatches(candidate?.contextTextMatches, 8), + locatorMatches: compactBatchSourceMatches(candidate?.locatorMatches, 6), + })); +} + +function compactBatchSourceMatches(matches, limit) { + if (!Array.isArray(matches)) return undefined; + return matches.slice(0, limit).map(compactBatchSourceMatch).filter(Boolean); +} + +function compactBatchSourceMatch(match) { + if (!match || typeof match !== 'object') return null; + return { + file: compactBatchString(match.relativeFile || match.file), + line: normalizeBatchNumber(match.line), + column: normalizeBatchNumber(match.column), + kind: compactBatchString(match.kind), + reason: compactBatchString(match.reason || match.kind), + status: compactBatchString(match.status), }; } @@ -311,25 +377,77 @@ function compactBatchOp(op) { contextRef: op.contextRef, tag: op.tag, elementId: op.elementId, - classes: op.classes, + classes: compactBatchStringList(op.classes, 24), originalText: op.originalText, newText: op.newText, deleted: op.deleted === true || undefined, - sourceHint: op.sourceHint, + sourceHint: normalizeBatchSourceHint(op.sourceHint), leaf: compactContextForBatch(op.leaf), - nearbyEditableTexts: Array.isArray(op.nearbyEditableTexts) ? op.nearbyEditableTexts.slice(0, 8) : [], + nearbyEditableTexts: compactNearbyBatchTexts(op.nearbyEditableTexts), container: compactContextForBatch(op.container), - contextHints: Array.isArray(op.contextHints) ? op.contextHints.slice(0, 12) : [], + contextHints: compactBatchStringList(op.contextHints, 12), }; } +function normalizeBatchSourceHint(hint) { + if (!hint || typeof hint !== 'object') return null; + let line = normalizeBatchNumber(hint.line); + let column = normalizeBatchNumber(hint.column); + if ((line === null || column === null) && typeof hint.loc === 'string') { + const match = hint.loc.match(/^(\d+)(?::(\d+))?/); + if (match) { + line = Number(match[1]); + if (match[2]) column = Number(match[2]); + } + } + return { + file: compactBatchString(hint.file) || '', + loc: compactBatchString(hint.loc) || '', + line, + column, + }; +} + +function normalizeBatchNumber(value) { + if (value === null || value === undefined || value === '') return null; + const number = Number(value); + return Number.isFinite(number) ? number : null; +} + +function normalizeOptionalBatchNumber(value) { + const number = normalizeBatchNumber(value); + return number === null ? undefined : number; +} + +function compactNearbyBatchTexts(items) { + return (Array.isArray(items) ? items : []) + .slice(0, 8) + .map((item) => typeof item === 'string' ? { text: truncate(item, BATCH_OP_TEXT_LIMIT) } : { + ref: compactBatchString(item?.ref), + tag: compactBatchString(item?.tag), + classes: compactBatchStringList(item?.classes, 24), + text: compactBatchString(item?.text), + }); +} + +function compactBatchStringList(items, limit) { + return (Array.isArray(items) ? items : []) + .slice(0, limit) + .filter((item) => typeof item === 'string') + .map((item) => truncate(item, BATCH_OP_TEXT_LIMIT)); +} + +function compactBatchString(value) { + return typeof value === 'string' ? truncate(value, BATCH_OP_TEXT_LIMIT) : undefined; +} + function compactContextForBatch(value) { if (!value || typeof value !== 'object') return value || null; return { - ref: value.ref, - tagName: value.tagName, - id: value.id, - classes: value.classes, + ref: compactBatchString(value.ref), + tagName: compactBatchString(value.tagName), + id: compactBatchString(value.id), + classes: compactBatchStringList(value.classes, 24), textContent: truncate(value.textContent, 900), outerHTML: truncate(stripLiveRuntimeHtml(value.outerHTML), 1800), }; @@ -470,12 +588,11 @@ function runClaude(prompt, { cwd, env, resultPath, logPath, timeoutMs = DEFAULT_ if (env.IMPECCABLE_LIVE_COPY_AGENT_MODEL) { args.push('--model', env.IMPECCABLE_LIVE_COPY_AGENT_MODEL); } - args.push(prompt); // Forward env as-is so CLAUDE_CODE_OAUTH_TOKEN and ANTHROPIC_API_KEY flow // through. On macOS, `claude /login` stores creds in the Keychain, which a // non-TTY subprocess cannot read; setting CLAUDE_CODE_OAUTH_TOKEN (via // `claude setup-token`) is the supported headless auth path. - return runAgentProcess('claude', args, '', { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); + return runAgentProcess('claude', args, prompt, { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); } function runAgentProcess(command, args, stdin, { cwd, env, logPath, timeoutMs, mirrorOutputPath }) { diff --git a/.trae/skills/impeccable/scripts/live-copy-edit-agent.mjs b/.trae/skills/impeccable/scripts/live-copy-edit-agent.mjs index 313ed7f10..febc8456d 100644 --- a/.trae/skills/impeccable/scripts/live-copy-edit-agent.mjs +++ b/.trae/skills/impeccable/scripts/live-copy-edit-agent.mjs @@ -14,10 +14,12 @@ import path from 'node:path'; import { createRequire } from 'node:module'; const DEFAULT_TIMEOUT_MS = 60_000; +const BATCH_OP_TEXT_LIMIT = 240; const require = createRequire(import.meta.url); export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { - const repairLines = batch?.repair ? [ + const compactBatch = compactBatchForPrompt(batch); + const repairLines = compactBatch.repair ? [ '', 'Repair mode:', '- The previous Apply attempt changed source, but validation failed.', @@ -28,7 +30,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { '- If failures or candidates show edited text is also a lookup key, update coupled count, animation, icon, image, asset, style, or metadata keys in the current source, or fail that entry without partial edits.', '- Keep failed and notes as arrays.', '- Return the same canonical JSON shape after repair.', - JSON.stringify(batch.repair, null, 2), + JSON.stringify(compactBatch.repair, null, 2), ] : []; return [ 'You are the Impeccable staged copy-edit batch applier.', @@ -80,7 +82,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { ...repairLines, '', 'Staged copy-edit batch:', - JSON.stringify(compactBatchForPrompt(batch), null, 2), + JSON.stringify(compactBatch, null, 2), ].join('\n'); } @@ -292,7 +294,7 @@ function readManualEditValidationScript(cwd) { function compactBatchForPrompt(batch) { return { pageUrl: batch?.pageUrl || null, - repair: batch?.repair || undefined, + repair: compactBatchRepair(batch?.repair), entries: (batch?.entries || []).map((entry) => ({ id: entry.id, pageUrl: entry.pageUrl, @@ -300,7 +302,71 @@ function compactBatchForPrompt(batch) { element: compactContextForBatch(entry.element), ops: (entry.ops || []).map(compactBatchOp), })), - candidates: batch?.candidates || [], + candidates: compactBatchCandidates(batch?.candidates), + }; +} + +function compactBatchRepair(repair) { + if (!repair || typeof repair !== 'object') return undefined; + return { + status: compactBatchString(repair.status), + attempt: normalizeOptionalBatchNumber(repair.attempt), + attempts: normalizeOptionalBatchNumber(repair.attempts), + maxAttempts: normalizeOptionalBatchNumber(repair.maxAttempts), + reason: compactBatchString(repair.reason), + transactionId: compactBatchString(repair.transactionId), + pageUrl: compactBatchString(repair.pageUrl), + failures: compactBatchDiagnostics(repair.failures), + files: compactBatchStringList(repair.files, 20), + }; +} + +function compactBatchDiagnostics(items, depth = 0) { + if (!Array.isArray(items)) return undefined; + return items.slice(0, 12).map((item) => ({ + entryId: compactBatchString(item?.entryId || item?.id), + reason: compactBatchString(item?.reason || item?.kind), + detail: compactBatchString(item?.detail), + message: compactBatchString(item?.message), + file: compactBatchString(item?.file || item?.relativeFile), + line: normalizeOptionalBatchNumber(item?.line), + ref: compactBatchString(item?.ref), + marker: compactBatchString(item?.marker), + files: compactBatchStringList(item?.files, 8), + candidates: depth < 2 ? compactBatchSourceMatches(item?.candidates, 8) : undefined, + failures: depth < 2 ? compactBatchDiagnostics(item?.failures, depth + 1) : undefined, + checks: depth < 2 ? compactBatchDiagnostics(item?.checks, depth + 1) : undefined, + })); +} + +function compactBatchCandidates(candidates) { + return (Array.isArray(candidates) ? candidates : []) + .slice(0, 24) + .map((candidate) => ({ + entryId: compactBatchString(candidate?.entryId), + ref: compactBatchString(candidate?.ref), + sourceHint: compactBatchSourceMatch(candidate?.sourceHint), + textMatches: compactBatchSourceMatches(candidate?.textMatches, 8), + objectKeyMatches: compactBatchSourceMatches(candidate?.objectKeyMatches, 8), + contextTextMatches: compactBatchSourceMatches(candidate?.contextTextMatches, 8), + locatorMatches: compactBatchSourceMatches(candidate?.locatorMatches, 6), + })); +} + +function compactBatchSourceMatches(matches, limit) { + if (!Array.isArray(matches)) return undefined; + return matches.slice(0, limit).map(compactBatchSourceMatch).filter(Boolean); +} + +function compactBatchSourceMatch(match) { + if (!match || typeof match !== 'object') return null; + return { + file: compactBatchString(match.relativeFile || match.file), + line: normalizeBatchNumber(match.line), + column: normalizeBatchNumber(match.column), + kind: compactBatchString(match.kind), + reason: compactBatchString(match.reason || match.kind), + status: compactBatchString(match.status), }; } @@ -311,25 +377,77 @@ function compactBatchOp(op) { contextRef: op.contextRef, tag: op.tag, elementId: op.elementId, - classes: op.classes, + classes: compactBatchStringList(op.classes, 24), originalText: op.originalText, newText: op.newText, deleted: op.deleted === true || undefined, - sourceHint: op.sourceHint, + sourceHint: normalizeBatchSourceHint(op.sourceHint), leaf: compactContextForBatch(op.leaf), - nearbyEditableTexts: Array.isArray(op.nearbyEditableTexts) ? op.nearbyEditableTexts.slice(0, 8) : [], + nearbyEditableTexts: compactNearbyBatchTexts(op.nearbyEditableTexts), container: compactContextForBatch(op.container), - contextHints: Array.isArray(op.contextHints) ? op.contextHints.slice(0, 12) : [], + contextHints: compactBatchStringList(op.contextHints, 12), }; } +function normalizeBatchSourceHint(hint) { + if (!hint || typeof hint !== 'object') return null; + let line = normalizeBatchNumber(hint.line); + let column = normalizeBatchNumber(hint.column); + if ((line === null || column === null) && typeof hint.loc === 'string') { + const match = hint.loc.match(/^(\d+)(?::(\d+))?/); + if (match) { + line = Number(match[1]); + if (match[2]) column = Number(match[2]); + } + } + return { + file: compactBatchString(hint.file) || '', + loc: compactBatchString(hint.loc) || '', + line, + column, + }; +} + +function normalizeBatchNumber(value) { + if (value === null || value === undefined || value === '') return null; + const number = Number(value); + return Number.isFinite(number) ? number : null; +} + +function normalizeOptionalBatchNumber(value) { + const number = normalizeBatchNumber(value); + return number === null ? undefined : number; +} + +function compactNearbyBatchTexts(items) { + return (Array.isArray(items) ? items : []) + .slice(0, 8) + .map((item) => typeof item === 'string' ? { text: truncate(item, BATCH_OP_TEXT_LIMIT) } : { + ref: compactBatchString(item?.ref), + tag: compactBatchString(item?.tag), + classes: compactBatchStringList(item?.classes, 24), + text: compactBatchString(item?.text), + }); +} + +function compactBatchStringList(items, limit) { + return (Array.isArray(items) ? items : []) + .slice(0, limit) + .filter((item) => typeof item === 'string') + .map((item) => truncate(item, BATCH_OP_TEXT_LIMIT)); +} + +function compactBatchString(value) { + return typeof value === 'string' ? truncate(value, BATCH_OP_TEXT_LIMIT) : undefined; +} + function compactContextForBatch(value) { if (!value || typeof value !== 'object') return value || null; return { - ref: value.ref, - tagName: value.tagName, - id: value.id, - classes: value.classes, + ref: compactBatchString(value.ref), + tagName: compactBatchString(value.tagName), + id: compactBatchString(value.id), + classes: compactBatchStringList(value.classes, 24), textContent: truncate(value.textContent, 900), outerHTML: truncate(stripLiveRuntimeHtml(value.outerHTML), 1800), }; @@ -470,12 +588,11 @@ function runClaude(prompt, { cwd, env, resultPath, logPath, timeoutMs = DEFAULT_ if (env.IMPECCABLE_LIVE_COPY_AGENT_MODEL) { args.push('--model', env.IMPECCABLE_LIVE_COPY_AGENT_MODEL); } - args.push(prompt); // Forward env as-is so CLAUDE_CODE_OAUTH_TOKEN and ANTHROPIC_API_KEY flow // through. On macOS, `claude /login` stores creds in the Keychain, which a // non-TTY subprocess cannot read; setting CLAUDE_CODE_OAUTH_TOKEN (via // `claude setup-token`) is the supported headless auth path. - return runAgentProcess('claude', args, '', { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); + return runAgentProcess('claude', args, prompt, { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); } function runAgentProcess(command, args, stdin, { cwd, env, logPath, timeoutMs, mirrorOutputPath }) { diff --git a/.vibe/skills/impeccable/scripts/live-copy-edit-agent.mjs b/.vibe/skills/impeccable/scripts/live-copy-edit-agent.mjs index 313ed7f10..febc8456d 100644 --- a/.vibe/skills/impeccable/scripts/live-copy-edit-agent.mjs +++ b/.vibe/skills/impeccable/scripts/live-copy-edit-agent.mjs @@ -14,10 +14,12 @@ import path from 'node:path'; import { createRequire } from 'node:module'; const DEFAULT_TIMEOUT_MS = 60_000; +const BATCH_OP_TEXT_LIMIT = 240; const require = createRequire(import.meta.url); export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { - const repairLines = batch?.repair ? [ + const compactBatch = compactBatchForPrompt(batch); + const repairLines = compactBatch.repair ? [ '', 'Repair mode:', '- The previous Apply attempt changed source, but validation failed.', @@ -28,7 +30,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { '- If failures or candidates show edited text is also a lookup key, update coupled count, animation, icon, image, asset, style, or metadata keys in the current source, or fail that entry without partial edits.', '- Keep failed and notes as arrays.', '- Return the same canonical JSON shape after repair.', - JSON.stringify(batch.repair, null, 2), + JSON.stringify(compactBatch.repair, null, 2), ] : []; return [ 'You are the Impeccable staged copy-edit batch applier.', @@ -80,7 +82,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { ...repairLines, '', 'Staged copy-edit batch:', - JSON.stringify(compactBatchForPrompt(batch), null, 2), + JSON.stringify(compactBatch, null, 2), ].join('\n'); } @@ -292,7 +294,7 @@ function readManualEditValidationScript(cwd) { function compactBatchForPrompt(batch) { return { pageUrl: batch?.pageUrl || null, - repair: batch?.repair || undefined, + repair: compactBatchRepair(batch?.repair), entries: (batch?.entries || []).map((entry) => ({ id: entry.id, pageUrl: entry.pageUrl, @@ -300,7 +302,71 @@ function compactBatchForPrompt(batch) { element: compactContextForBatch(entry.element), ops: (entry.ops || []).map(compactBatchOp), })), - candidates: batch?.candidates || [], + candidates: compactBatchCandidates(batch?.candidates), + }; +} + +function compactBatchRepair(repair) { + if (!repair || typeof repair !== 'object') return undefined; + return { + status: compactBatchString(repair.status), + attempt: normalizeOptionalBatchNumber(repair.attempt), + attempts: normalizeOptionalBatchNumber(repair.attempts), + maxAttempts: normalizeOptionalBatchNumber(repair.maxAttempts), + reason: compactBatchString(repair.reason), + transactionId: compactBatchString(repair.transactionId), + pageUrl: compactBatchString(repair.pageUrl), + failures: compactBatchDiagnostics(repair.failures), + files: compactBatchStringList(repair.files, 20), + }; +} + +function compactBatchDiagnostics(items, depth = 0) { + if (!Array.isArray(items)) return undefined; + return items.slice(0, 12).map((item) => ({ + entryId: compactBatchString(item?.entryId || item?.id), + reason: compactBatchString(item?.reason || item?.kind), + detail: compactBatchString(item?.detail), + message: compactBatchString(item?.message), + file: compactBatchString(item?.file || item?.relativeFile), + line: normalizeOptionalBatchNumber(item?.line), + ref: compactBatchString(item?.ref), + marker: compactBatchString(item?.marker), + files: compactBatchStringList(item?.files, 8), + candidates: depth < 2 ? compactBatchSourceMatches(item?.candidates, 8) : undefined, + failures: depth < 2 ? compactBatchDiagnostics(item?.failures, depth + 1) : undefined, + checks: depth < 2 ? compactBatchDiagnostics(item?.checks, depth + 1) : undefined, + })); +} + +function compactBatchCandidates(candidates) { + return (Array.isArray(candidates) ? candidates : []) + .slice(0, 24) + .map((candidate) => ({ + entryId: compactBatchString(candidate?.entryId), + ref: compactBatchString(candidate?.ref), + sourceHint: compactBatchSourceMatch(candidate?.sourceHint), + textMatches: compactBatchSourceMatches(candidate?.textMatches, 8), + objectKeyMatches: compactBatchSourceMatches(candidate?.objectKeyMatches, 8), + contextTextMatches: compactBatchSourceMatches(candidate?.contextTextMatches, 8), + locatorMatches: compactBatchSourceMatches(candidate?.locatorMatches, 6), + })); +} + +function compactBatchSourceMatches(matches, limit) { + if (!Array.isArray(matches)) return undefined; + return matches.slice(0, limit).map(compactBatchSourceMatch).filter(Boolean); +} + +function compactBatchSourceMatch(match) { + if (!match || typeof match !== 'object') return null; + return { + file: compactBatchString(match.relativeFile || match.file), + line: normalizeBatchNumber(match.line), + column: normalizeBatchNumber(match.column), + kind: compactBatchString(match.kind), + reason: compactBatchString(match.reason || match.kind), + status: compactBatchString(match.status), }; } @@ -311,25 +377,77 @@ function compactBatchOp(op) { contextRef: op.contextRef, tag: op.tag, elementId: op.elementId, - classes: op.classes, + classes: compactBatchStringList(op.classes, 24), originalText: op.originalText, newText: op.newText, deleted: op.deleted === true || undefined, - sourceHint: op.sourceHint, + sourceHint: normalizeBatchSourceHint(op.sourceHint), leaf: compactContextForBatch(op.leaf), - nearbyEditableTexts: Array.isArray(op.nearbyEditableTexts) ? op.nearbyEditableTexts.slice(0, 8) : [], + nearbyEditableTexts: compactNearbyBatchTexts(op.nearbyEditableTexts), container: compactContextForBatch(op.container), - contextHints: Array.isArray(op.contextHints) ? op.contextHints.slice(0, 12) : [], + contextHints: compactBatchStringList(op.contextHints, 12), }; } +function normalizeBatchSourceHint(hint) { + if (!hint || typeof hint !== 'object') return null; + let line = normalizeBatchNumber(hint.line); + let column = normalizeBatchNumber(hint.column); + if ((line === null || column === null) && typeof hint.loc === 'string') { + const match = hint.loc.match(/^(\d+)(?::(\d+))?/); + if (match) { + line = Number(match[1]); + if (match[2]) column = Number(match[2]); + } + } + return { + file: compactBatchString(hint.file) || '', + loc: compactBatchString(hint.loc) || '', + line, + column, + }; +} + +function normalizeBatchNumber(value) { + if (value === null || value === undefined || value === '') return null; + const number = Number(value); + return Number.isFinite(number) ? number : null; +} + +function normalizeOptionalBatchNumber(value) { + const number = normalizeBatchNumber(value); + return number === null ? undefined : number; +} + +function compactNearbyBatchTexts(items) { + return (Array.isArray(items) ? items : []) + .slice(0, 8) + .map((item) => typeof item === 'string' ? { text: truncate(item, BATCH_OP_TEXT_LIMIT) } : { + ref: compactBatchString(item?.ref), + tag: compactBatchString(item?.tag), + classes: compactBatchStringList(item?.classes, 24), + text: compactBatchString(item?.text), + }); +} + +function compactBatchStringList(items, limit) { + return (Array.isArray(items) ? items : []) + .slice(0, limit) + .filter((item) => typeof item === 'string') + .map((item) => truncate(item, BATCH_OP_TEXT_LIMIT)); +} + +function compactBatchString(value) { + return typeof value === 'string' ? truncate(value, BATCH_OP_TEXT_LIMIT) : undefined; +} + function compactContextForBatch(value) { if (!value || typeof value !== 'object') return value || null; return { - ref: value.ref, - tagName: value.tagName, - id: value.id, - classes: value.classes, + ref: compactBatchString(value.ref), + tagName: compactBatchString(value.tagName), + id: compactBatchString(value.id), + classes: compactBatchStringList(value.classes, 24), textContent: truncate(value.textContent, 900), outerHTML: truncate(stripLiveRuntimeHtml(value.outerHTML), 1800), }; @@ -470,12 +588,11 @@ function runClaude(prompt, { cwd, env, resultPath, logPath, timeoutMs = DEFAULT_ if (env.IMPECCABLE_LIVE_COPY_AGENT_MODEL) { args.push('--model', env.IMPECCABLE_LIVE_COPY_AGENT_MODEL); } - args.push(prompt); // Forward env as-is so CLAUDE_CODE_OAUTH_TOKEN and ANTHROPIC_API_KEY flow // through. On macOS, `claude /login` stores creds in the Keychain, which a // non-TTY subprocess cannot read; setting CLAUDE_CODE_OAUTH_TOKEN (via // `claude setup-token`) is the supported headless auth path. - return runAgentProcess('claude', args, '', { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); + return runAgentProcess('claude', args, prompt, { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); } function runAgentProcess(command, args, stdin, { cwd, env, logPath, timeoutMs, mirrorOutputPath }) { diff --git a/plugin/skills/impeccable/scripts/live-copy-edit-agent.mjs b/plugin/skills/impeccable/scripts/live-copy-edit-agent.mjs index 313ed7f10..febc8456d 100644 --- a/plugin/skills/impeccable/scripts/live-copy-edit-agent.mjs +++ b/plugin/skills/impeccable/scripts/live-copy-edit-agent.mjs @@ -14,10 +14,12 @@ import path from 'node:path'; import { createRequire } from 'node:module'; const DEFAULT_TIMEOUT_MS = 60_000; +const BATCH_OP_TEXT_LIMIT = 240; const require = createRequire(import.meta.url); export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { - const repairLines = batch?.repair ? [ + const compactBatch = compactBatchForPrompt(batch); + const repairLines = compactBatch.repair ? [ '', 'Repair mode:', '- The previous Apply attempt changed source, but validation failed.', @@ -28,7 +30,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { '- If failures or candidates show edited text is also a lookup key, update coupled count, animation, icon, image, asset, style, or metadata keys in the current source, or fail that entry without partial edits.', '- Keep failed and notes as arrays.', '- Return the same canonical JSON shape after repair.', - JSON.stringify(batch.repair, null, 2), + JSON.stringify(compactBatch.repair, null, 2), ] : []; return [ 'You are the Impeccable staged copy-edit batch applier.', @@ -80,7 +82,7 @@ export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) { ...repairLines, '', 'Staged copy-edit batch:', - JSON.stringify(compactBatchForPrompt(batch), null, 2), + JSON.stringify(compactBatch, null, 2), ].join('\n'); } @@ -292,7 +294,7 @@ function readManualEditValidationScript(cwd) { function compactBatchForPrompt(batch) { return { pageUrl: batch?.pageUrl || null, - repair: batch?.repair || undefined, + repair: compactBatchRepair(batch?.repair), entries: (batch?.entries || []).map((entry) => ({ id: entry.id, pageUrl: entry.pageUrl, @@ -300,7 +302,71 @@ function compactBatchForPrompt(batch) { element: compactContextForBatch(entry.element), ops: (entry.ops || []).map(compactBatchOp), })), - candidates: batch?.candidates || [], + candidates: compactBatchCandidates(batch?.candidates), + }; +} + +function compactBatchRepair(repair) { + if (!repair || typeof repair !== 'object') return undefined; + return { + status: compactBatchString(repair.status), + attempt: normalizeOptionalBatchNumber(repair.attempt), + attempts: normalizeOptionalBatchNumber(repair.attempts), + maxAttempts: normalizeOptionalBatchNumber(repair.maxAttempts), + reason: compactBatchString(repair.reason), + transactionId: compactBatchString(repair.transactionId), + pageUrl: compactBatchString(repair.pageUrl), + failures: compactBatchDiagnostics(repair.failures), + files: compactBatchStringList(repair.files, 20), + }; +} + +function compactBatchDiagnostics(items, depth = 0) { + if (!Array.isArray(items)) return undefined; + return items.slice(0, 12).map((item) => ({ + entryId: compactBatchString(item?.entryId || item?.id), + reason: compactBatchString(item?.reason || item?.kind), + detail: compactBatchString(item?.detail), + message: compactBatchString(item?.message), + file: compactBatchString(item?.file || item?.relativeFile), + line: normalizeOptionalBatchNumber(item?.line), + ref: compactBatchString(item?.ref), + marker: compactBatchString(item?.marker), + files: compactBatchStringList(item?.files, 8), + candidates: depth < 2 ? compactBatchSourceMatches(item?.candidates, 8) : undefined, + failures: depth < 2 ? compactBatchDiagnostics(item?.failures, depth + 1) : undefined, + checks: depth < 2 ? compactBatchDiagnostics(item?.checks, depth + 1) : undefined, + })); +} + +function compactBatchCandidates(candidates) { + return (Array.isArray(candidates) ? candidates : []) + .slice(0, 24) + .map((candidate) => ({ + entryId: compactBatchString(candidate?.entryId), + ref: compactBatchString(candidate?.ref), + sourceHint: compactBatchSourceMatch(candidate?.sourceHint), + textMatches: compactBatchSourceMatches(candidate?.textMatches, 8), + objectKeyMatches: compactBatchSourceMatches(candidate?.objectKeyMatches, 8), + contextTextMatches: compactBatchSourceMatches(candidate?.contextTextMatches, 8), + locatorMatches: compactBatchSourceMatches(candidate?.locatorMatches, 6), + })); +} + +function compactBatchSourceMatches(matches, limit) { + if (!Array.isArray(matches)) return undefined; + return matches.slice(0, limit).map(compactBatchSourceMatch).filter(Boolean); +} + +function compactBatchSourceMatch(match) { + if (!match || typeof match !== 'object') return null; + return { + file: compactBatchString(match.relativeFile || match.file), + line: normalizeBatchNumber(match.line), + column: normalizeBatchNumber(match.column), + kind: compactBatchString(match.kind), + reason: compactBatchString(match.reason || match.kind), + status: compactBatchString(match.status), }; } @@ -311,25 +377,77 @@ function compactBatchOp(op) { contextRef: op.contextRef, tag: op.tag, elementId: op.elementId, - classes: op.classes, + classes: compactBatchStringList(op.classes, 24), originalText: op.originalText, newText: op.newText, deleted: op.deleted === true || undefined, - sourceHint: op.sourceHint, + sourceHint: normalizeBatchSourceHint(op.sourceHint), leaf: compactContextForBatch(op.leaf), - nearbyEditableTexts: Array.isArray(op.nearbyEditableTexts) ? op.nearbyEditableTexts.slice(0, 8) : [], + nearbyEditableTexts: compactNearbyBatchTexts(op.nearbyEditableTexts), container: compactContextForBatch(op.container), - contextHints: Array.isArray(op.contextHints) ? op.contextHints.slice(0, 12) : [], + contextHints: compactBatchStringList(op.contextHints, 12), }; } +function normalizeBatchSourceHint(hint) { + if (!hint || typeof hint !== 'object') return null; + let line = normalizeBatchNumber(hint.line); + let column = normalizeBatchNumber(hint.column); + if ((line === null || column === null) && typeof hint.loc === 'string') { + const match = hint.loc.match(/^(\d+)(?::(\d+))?/); + if (match) { + line = Number(match[1]); + if (match[2]) column = Number(match[2]); + } + } + return { + file: compactBatchString(hint.file) || '', + loc: compactBatchString(hint.loc) || '', + line, + column, + }; +} + +function normalizeBatchNumber(value) { + if (value === null || value === undefined || value === '') return null; + const number = Number(value); + return Number.isFinite(number) ? number : null; +} + +function normalizeOptionalBatchNumber(value) { + const number = normalizeBatchNumber(value); + return number === null ? undefined : number; +} + +function compactNearbyBatchTexts(items) { + return (Array.isArray(items) ? items : []) + .slice(0, 8) + .map((item) => typeof item === 'string' ? { text: truncate(item, BATCH_OP_TEXT_LIMIT) } : { + ref: compactBatchString(item?.ref), + tag: compactBatchString(item?.tag), + classes: compactBatchStringList(item?.classes, 24), + text: compactBatchString(item?.text), + }); +} + +function compactBatchStringList(items, limit) { + return (Array.isArray(items) ? items : []) + .slice(0, limit) + .filter((item) => typeof item === 'string') + .map((item) => truncate(item, BATCH_OP_TEXT_LIMIT)); +} + +function compactBatchString(value) { + return typeof value === 'string' ? truncate(value, BATCH_OP_TEXT_LIMIT) : undefined; +} + function compactContextForBatch(value) { if (!value || typeof value !== 'object') return value || null; return { - ref: value.ref, - tagName: value.tagName, - id: value.id, - classes: value.classes, + ref: compactBatchString(value.ref), + tagName: compactBatchString(value.tagName), + id: compactBatchString(value.id), + classes: compactBatchStringList(value.classes, 24), textContent: truncate(value.textContent, 900), outerHTML: truncate(stripLiveRuntimeHtml(value.outerHTML), 1800), }; @@ -470,12 +588,11 @@ function runClaude(prompt, { cwd, env, resultPath, logPath, timeoutMs = DEFAULT_ if (env.IMPECCABLE_LIVE_COPY_AGENT_MODEL) { args.push('--model', env.IMPECCABLE_LIVE_COPY_AGENT_MODEL); } - args.push(prompt); // Forward env as-is so CLAUDE_CODE_OAUTH_TOKEN and ANTHROPIC_API_KEY flow // through. On macOS, `claude /login` stores creds in the Keychain, which a // non-TTY subprocess cannot read; setting CLAUDE_CODE_OAUTH_TOKEN (via // `claude setup-token`) is the supported headless auth path. - return runAgentProcess('claude', args, '', { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); + return runAgentProcess('claude', args, prompt, { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath }); } function runAgentProcess(command, args, stdin, { cwd, env, logPath, timeoutMs, mirrorOutputPath }) { From 5f7b001cbe28f23e51d6c88a84eb91b58e3a9113 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Mon, 10 Aug 2026 12:35:55 -0700 Subject: [PATCH 24/29] Fix: measure gradient body grounds instead of assuming white (low-contrast false positives) (#557) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: measure gradient body grounds instead of assuming white (browser mode) A page whose ground is set via background: linear-gradient(...) on body leaves backgroundColor transparent, and resolveBackground assumed white for any body/html-level gradient. In a real browser that assumption is wrong: the shorthand is always decomposed there, so reaching that branch means the ground truly is the gradient. On a dark oklch gradient ground (impeccable.style's lacquer) this turned every light-on-dark text into a ~1.3:1 "on #ffffff" low-contrast finding, ~120 false positives on one site. Browser mode now returns null so the caller measures against the actual gradient stops; the white assumption stays for jsdom, where the undecomposed-shorthand rationale still holds. Gradient stops also now parse modern color syntax: computed backgroundImage keeps oklch()/oklab()/hsl()/hwb() stops as authored, and parseGradientColors only read rgb()/hex, so a token-driven gradient ground was invisible even once the walk deferred to it. New parseGradientColorsModern routes those stops through parseAnyColor. Covered by a Puppeteer fixture (dark oklch body gradient): light text on the ground must not flag, muted dark-gray ink must, proving the stops are measured rather than the checks silently skipping. Prepared with AI assistance (Claude Code), on maintainer instruction. Co-Authored-By: Claude Fable 5 * Composite translucent layers over gradient stops; parse modern glow stops Review fixes from PR #557's automated reviews, applied with AI assistance (Claude Code): - Cursor Bugbot found the new browser-mode early return discarded the translucent ancestors resolveBackground had collected: text on a frosted wash over a body gradient was measured against raw stops. resolveGradientStops now collects translucent layers during its own walk (through readCascadeBackgroundColor, extracted so both walks read surfaces identically) and composites every stop under them. - Copilot flagged the other legacy parseGradientColors call sites. The glow-context fallback now uses parseGradientColorsModern, since body gradients reach it more often after this change. The AI-palette rule and the injected analytic sampler stay on the legacy parser deliberately: the former is a rule-behavior expansion deserving its own fixtures, the latter degrades to pixel sampling or a skip. - Greptile asked for standard fixture structure: the fixture now has labeled flag/pass cases (3 flag, 5 pass) including the frosted-wash pair that locks the overlay compositing in both directions and a legacy hex-stop gradient guarding the original parser path. The test scopes itself to the DOM path via visualContrast: false, the suite's established pattern; the screenshot sampler is a separate subsystem with its own coverage. Co-Authored-By: Claude Fable 5 * Pin gradient-ground flag cases to their snippet signatures Bugbot follow-up: a count-only assertion let an offsetting miss and false positive cancel, especially the frosted pair. Each flag case now asserts its full text-on-background signature, so the frosted case must measure against the composited wash and the count guard excludes any pass case flagging in its place. Applied with AI assistance (Claude Code). Co-Authored-By: Claude Fable 5 * Comments: the static path is the custom engine now, not jsdom jsdom left the dependency tree when the static-html engine (StaticElement + css-cascade.mjs) replaced it, and that engine does decompose the background shorthand, so the comments this PR added were dated in both name and rationale. Only comments touched by this PR are renamed; the ~40 legacy jsdom mentions elsewhere in checks.mjs are a separate sweep. Applied with AI assistance (Claude Code). Co-Authored-By: Claude Fable 5 * Static engine: measure body gradients too, dropping the white assumption Follow-up to the browser-mode fix: the white assumption for body/html gradients was a jsdom guard, and jsdom is gone. The static cascade decomposes the background shorthand (expandStaticDeclaration) and preserves var() colors for later resolution, so a missing solid under a body gradient is now as real in static mode as in a browser — and the static engine had the identical false-positive class (light text on a dark gradient ground flagged "on #ffffff") while missing the muted-ink true positives on the same page. The old catastrophic case cannot recur: opaque stops fully cover any hidden solid (they are the ground), alpha stops composite over the resolved base or the white canvas default, and unresolvable stops drop rather than guess. Static twin of the browser test added over the same fixture; the full suite, the url()-ancestor guard, and a source scan of impeccable.style (0 low-contrast findings) all stay clean. Applied with AI assistance (Claude Code). Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- cli/engine/detect-antipatterns-browser.js | 120 +++++++++++++----- cli/engine/rules/checks.mjs | 120 +++++++++++++----- tests/detect-antipatterns-browser.test.mjs | 25 ++++ tests/detect-antipatterns-fixtures.test.mjs | 17 +++ .../antipatterns/dark-gradient-ground.html | 97 ++++++++++++++ 5 files changed, 319 insertions(+), 60 deletions(-) create mode 100644 tests/fixtures/antipatterns/dark-gradient-ground.html diff --git a/cli/engine/detect-antipatterns-browser.js b/cli/engine/detect-antipatterns-browser.js index 400c9294b..a3422ff6c 100644 --- a/cli/engine/detect-antipatterns-browser.js +++ b/cli/engine/detect-antipatterns-browser.js @@ -2461,6 +2461,33 @@ function readOwnBackgroundColor(el, computedStyle) { return bg; } +// One element's background-color as the cascade walk sees it: computed style +// first (with the modern-color fallback), then, in static mode only, +// custom-prop resolution and the inline-shorthand peek. Shared by +// resolveBackground and resolveGradientStops so both walks read the same +// surfaces. +function readCascadeBackgroundColor(current, style, customPropMap) { + let bg = parseRgb(style.backgroundColor) || parseAnyColor(style.backgroundColor); + if (!DETECTOR_IS_BROWSER && (!bg || bg.a < 0.1)) { + // The static engine can return literal "var(--X)" / "oklch(...)" strings. + // Resolve through customPropMap so Tailwind v4 color tokens become RGB. + if (customPropMap) { + bg = parseColorResolved(style.backgroundColor, customPropMap); + } + if (!bg || bg.a < 0.1) { + // Inline-style fallback for colors the static cascade did not surface + // on backgroundColor. + const rawStyle = current.getAttribute?.('style') || ''; + const bgMatch = rawStyle.match(/background(?:-color)?\s*:\s*([^;]+)/i); + const inlineBg = bgMatch ? bgMatch[1].trim() : ''; + if (inlineBg && !/gradient/i.test(inlineBg) && !/url\s*\(/i.test(inlineBg)) { + bg = parseColorResolved(inlineBg, customPropMap) || parseAnyColor(inlineBg); + } + } + } + return bg; +} + function resolveBackground(el, win, customPropMap) { let current = el; // Translucent layers (0.1 < a < 1) found on the way down to an opaque @@ -2489,24 +2516,7 @@ function resolveBackground(el, win, customPropMap) { // body backgrounds. // Real browsers serialize wide-gamut computed values as oklab()/oklch() // (e.g. any color-mix() result), which plain parseRgb misses. - let bg = parseRgb(style.backgroundColor) || parseAnyColor(style.backgroundColor); - if (!DETECTOR_IS_BROWSER && (!bg || bg.a < 0.1)) { - // jsdom returns literal "var(--X)" / "oklch(...)" strings. Resolve - // through customPropMap so Tailwind v4 color tokens become RGB. - if (customPropMap) { - bg = parseColorResolved(style.backgroundColor, customPropMap); - } - if (!bg || bg.a < 0.1) { - // Inline-style fallback. jsdom doesn't decompose background - // shorthand, so colors set via inline style are otherwise invisible. - const rawStyle = current.getAttribute?.('style') || ''; - const bgMatch = rawStyle.match(/background(?:-color)?\s*:\s*([^;]+)/i); - const inlineBg = bgMatch ? bgMatch[1].trim() : ''; - if (inlineBg && !/gradient/i.test(inlineBg) && !/url\s*\(/i.test(inlineBg)) { - bg = parseColorResolved(inlineBg, customPropMap) || parseAnyColor(inlineBg); - } - } - } + const bg = readCascadeBackgroundColor(current, style, customPropMap); if (bg && bg.a > 0.1) { if (bg.a >= 0.99) return flatten(bg); @@ -2524,40 +2534,88 @@ function resolveBackground(el, win, customPropMap) { // • on other elements: bail to null and let the caller fall back // to gradient stops (gradient buttons / hero sections are real // bgs worth checking against). - if (hasGradientOrUrl) { - if (current.tagName === 'BODY' || current.tagName === 'HTML') { - return flatten({ r: 255, g: 255, b: 255, a: 1 }); - } - return null; - } + // A gradient or image with no solid color under it, at any level + // including body/html, means the visible ground is that layer itself. + // Return null so the caller measures against the actual gradient stops, + // or skips when nothing is parseable — skipping beats a wrong ratio. + // + // Body/html used to assume white here, a guard written for jsdom, which + // never decomposed the `background:` shorthand and so could not see the + // solid paper color a texture gradient usually sits on. It turned every + // light-on-dark page into a wall of low-contrast false positives (a dark + // oklch body gradient produced ~120 "on #ffffff" findings on one site). + // Both engines can see shorthand solids now — the browser natively, the + // static cascade via expandStaticDeclaration + var() resolution — so a + // missing solid is real, and the old failure case cannot recur: opaque + // stops fully cover any hidden solid (they ARE the ground), alpha stops + // composite over the resolved base or the white canvas default, and + // unresolvable stops drop rather than guess. + if (hasGradientOrUrl) return null; current = current.parentElement; } return flatten({ r: 255, g: 255, b: 255, a: 1 }); } +// parseGradientColors (shared) reads only the legacy serializations: rgb() +// and hex stops. Browsers keep modern-space stops in computed backgroundImage +// exactly as authored — `linear-gradient(oklch(7% 0.006 95), …)` stays oklch — +// which is what every token-driven page produces. Route those through +// parseAnyColor so a gradient ground is measurable rather than invisible. +function parseGradientColorsModern(bgImage) { + if (!bgImage || !/gradient/i.test(bgImage)) return []; + const colors = parseGradientColors(bgImage); + for (const m of bgImage.matchAll(/(?:oklch|oklab|hsla?|hwb)\(\s*[^()]*\)/gi)) { + const c = parseAnyColor(m[0]); + if (c) colors.push(c); + } + return colors; +} + // Walk parents looking for a gradient background and return its color stops. // Used as a fallback when resolveBackground() returns null because the // effective background is a gradient (no single solid color to compare against). +// Translucent solid layers found between the element and the gradient (frosted +// panels, glass washes) are composited over every stop, the same way +// resolveBackground flattens them over a solid base — raw stops alone would +// false-flag dark text on a light frosted wash over a dark gradient, and miss +// the inverse. function resolveGradientStops(el, win, customPropMap) { let current = el; + const overlays = []; while (current && current.nodeType === 1) { const style = DETECTOR_IS_BROWSER ? getComputedStyle(current) : win.getComputedStyle(current); const bgImage = style.backgroundImage || ''; let stops = null; if (bgImage && bgImage !== 'none' && /gradient/i.test(bgImage)) { - const parsed = parseGradientColors(bgImage); + const parsed = parseGradientColorsModern(bgImage); if (parsed.length > 0) stops = parsed; } if (!stops && !DETECTOR_IS_BROWSER) { - // jsdom doesn't decompose `background:` shorthand — peek at the raw inline style + // Static mode: peek at the raw inline style for gradients the cascade did not surface const rawStyle = current.getAttribute?.('style') || ''; const bgMatch = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); if (bgMatch && /gradient/i.test(bgMatch[1])) { - const parsed = parseGradientColors(bgMatch[1]); + const parsed = parseGradientColorsModern(bgMatch[1]); if (parsed.length > 0) stops = parsed; } } - if (stops) return compositeGradientStops(stops, current, win, customPropMap); + if (stops) { + const composited = compositeGradientStops(stops, current, win, customPropMap); + if (!composited || overlays.length === 0) return composited; + return composited.map(stop => { + let acc = stop; + for (let i = overlays.length - 1; i >= 0; i--) acc = compositeColorOver(overlays[i], acc); + return acc; + }); + } + const bg = readCascadeBackgroundColor(current, style, customPropMap); + if (bg && bg.a > 0.1) { + // An opaque surface above the gradient means the gradient never shows + // through here; resolveBackground would have returned it, so reaching + // this is defensive — bail rather than measure the wrong layer. + if (bg.a >= 0.99) return null; + overlays.push(bg); + } current = current.parentElement; } return null; @@ -3589,11 +3647,13 @@ function checkElementGlowDOM(el) { // If resolveBackground returns null (gradient), try to infer from the gradient colors let parentBg = el.parentElement ? resolveBackground(el.parentElement) : resolveBackground(el); if (!parentBg) { - // Gradient background — sample its colors to determine if it's dark + // Gradient background — sample its colors to determine if it's dark. + // Modern-syntax parsing matters here: body-level gradients now reach this + // fallback in browser mode, and their stops usually serialize as oklch. let cur = el.parentElement; while (cur && cur.nodeType === 1) { const bgImage = getComputedStyle(cur).backgroundImage || ''; - const gradColors = parseGradientColors(bgImage); + const gradColors = parseGradientColorsModern(bgImage); if (gradColors.length > 0) { // Average the gradient colors const avg = { r: 0, g: 0, b: 0 }; diff --git a/cli/engine/rules/checks.mjs b/cli/engine/rules/checks.mjs index aee24a947..57341b017 100644 --- a/cli/engine/rules/checks.mjs +++ b/cli/engine/rules/checks.mjs @@ -1670,6 +1670,33 @@ function readOwnBackgroundColor(el, computedStyle) { return bg; } +// One element's background-color as the cascade walk sees it: computed style +// first (with the modern-color fallback), then, in static mode only, +// custom-prop resolution and the inline-shorthand peek. Shared by +// resolveBackground and resolveGradientStops so both walks read the same +// surfaces. +function readCascadeBackgroundColor(current, style, customPropMap) { + let bg = parseRgb(style.backgroundColor) || parseAnyColor(style.backgroundColor); + if (!DETECTOR_IS_BROWSER && (!bg || bg.a < 0.1)) { + // The static engine can return literal "var(--X)" / "oklch(...)" strings. + // Resolve through customPropMap so Tailwind v4 color tokens become RGB. + if (customPropMap) { + bg = parseColorResolved(style.backgroundColor, customPropMap); + } + if (!bg || bg.a < 0.1) { + // Inline-style fallback for colors the static cascade did not surface + // on backgroundColor. + const rawStyle = current.getAttribute?.('style') || ''; + const bgMatch = rawStyle.match(/background(?:-color)?\s*:\s*([^;]+)/i); + const inlineBg = bgMatch ? bgMatch[1].trim() : ''; + if (inlineBg && !/gradient/i.test(inlineBg) && !/url\s*\(/i.test(inlineBg)) { + bg = parseColorResolved(inlineBg, customPropMap) || parseAnyColor(inlineBg); + } + } + } + return bg; +} + function resolveBackground(el, win, customPropMap) { let current = el; // Translucent layers (0.1 < a < 1) found on the way down to an opaque @@ -1698,24 +1725,7 @@ function resolveBackground(el, win, customPropMap) { // body backgrounds. // Real browsers serialize wide-gamut computed values as oklab()/oklch() // (e.g. any color-mix() result), which plain parseRgb misses. - let bg = parseRgb(style.backgroundColor) || parseAnyColor(style.backgroundColor); - if (!DETECTOR_IS_BROWSER && (!bg || bg.a < 0.1)) { - // jsdom returns literal "var(--X)" / "oklch(...)" strings. Resolve - // through customPropMap so Tailwind v4 color tokens become RGB. - if (customPropMap) { - bg = parseColorResolved(style.backgroundColor, customPropMap); - } - if (!bg || bg.a < 0.1) { - // Inline-style fallback. jsdom doesn't decompose background - // shorthand, so colors set via inline style are otherwise invisible. - const rawStyle = current.getAttribute?.('style') || ''; - const bgMatch = rawStyle.match(/background(?:-color)?\s*:\s*([^;]+)/i); - const inlineBg = bgMatch ? bgMatch[1].trim() : ''; - if (inlineBg && !/gradient/i.test(inlineBg) && !/url\s*\(/i.test(inlineBg)) { - bg = parseColorResolved(inlineBg, customPropMap) || parseAnyColor(inlineBg); - } - } - } + const bg = readCascadeBackgroundColor(current, style, customPropMap); if (bg && bg.a > 0.1) { if (bg.a >= 0.99) return flatten(bg); @@ -1733,40 +1743,88 @@ function resolveBackground(el, win, customPropMap) { // • on other elements: bail to null and let the caller fall back // to gradient stops (gradient buttons / hero sections are real // bgs worth checking against). - if (hasGradientOrUrl) { - if (current.tagName === 'BODY' || current.tagName === 'HTML') { - return flatten({ r: 255, g: 255, b: 255, a: 1 }); - } - return null; - } + // A gradient or image with no solid color under it, at any level + // including body/html, means the visible ground is that layer itself. + // Return null so the caller measures against the actual gradient stops, + // or skips when nothing is parseable — skipping beats a wrong ratio. + // + // Body/html used to assume white here, a guard written for jsdom, which + // never decomposed the `background:` shorthand and so could not see the + // solid paper color a texture gradient usually sits on. It turned every + // light-on-dark page into a wall of low-contrast false positives (a dark + // oklch body gradient produced ~120 "on #ffffff" findings on one site). + // Both engines can see shorthand solids now — the browser natively, the + // static cascade via expandStaticDeclaration + var() resolution — so a + // missing solid is real, and the old failure case cannot recur: opaque + // stops fully cover any hidden solid (they ARE the ground), alpha stops + // composite over the resolved base or the white canvas default, and + // unresolvable stops drop rather than guess. + if (hasGradientOrUrl) return null; current = current.parentElement; } return flatten({ r: 255, g: 255, b: 255, a: 1 }); } +// parseGradientColors (shared) reads only the legacy serializations: rgb() +// and hex stops. Browsers keep modern-space stops in computed backgroundImage +// exactly as authored — `linear-gradient(oklch(7% 0.006 95), …)` stays oklch — +// which is what every token-driven page produces. Route those through +// parseAnyColor so a gradient ground is measurable rather than invisible. +function parseGradientColorsModern(bgImage) { + if (!bgImage || !/gradient/i.test(bgImage)) return []; + const colors = parseGradientColors(bgImage); + for (const m of bgImage.matchAll(/(?:oklch|oklab|hsla?|hwb)\(\s*[^()]*\)/gi)) { + const c = parseAnyColor(m[0]); + if (c) colors.push(c); + } + return colors; +} + // Walk parents looking for a gradient background and return its color stops. // Used as a fallback when resolveBackground() returns null because the // effective background is a gradient (no single solid color to compare against). +// Translucent solid layers found between the element and the gradient (frosted +// panels, glass washes) are composited over every stop, the same way +// resolveBackground flattens them over a solid base — raw stops alone would +// false-flag dark text on a light frosted wash over a dark gradient, and miss +// the inverse. function resolveGradientStops(el, win, customPropMap) { let current = el; + const overlays = []; while (current && current.nodeType === 1) { const style = DETECTOR_IS_BROWSER ? getComputedStyle(current) : win.getComputedStyle(current); const bgImage = style.backgroundImage || ''; let stops = null; if (bgImage && bgImage !== 'none' && /gradient/i.test(bgImage)) { - const parsed = parseGradientColors(bgImage); + const parsed = parseGradientColorsModern(bgImage); if (parsed.length > 0) stops = parsed; } if (!stops && !DETECTOR_IS_BROWSER) { - // jsdom doesn't decompose `background:` shorthand — peek at the raw inline style + // Static mode: peek at the raw inline style for gradients the cascade did not surface const rawStyle = current.getAttribute?.('style') || ''; const bgMatch = rawStyle.match(/background(?:-image)?\s*:\s*([^;]+)/i); if (bgMatch && /gradient/i.test(bgMatch[1])) { - const parsed = parseGradientColors(bgMatch[1]); + const parsed = parseGradientColorsModern(bgMatch[1]); if (parsed.length > 0) stops = parsed; } } - if (stops) return compositeGradientStops(stops, current, win, customPropMap); + if (stops) { + const composited = compositeGradientStops(stops, current, win, customPropMap); + if (!composited || overlays.length === 0) return composited; + return composited.map(stop => { + let acc = stop; + for (let i = overlays.length - 1; i >= 0; i--) acc = compositeColorOver(overlays[i], acc); + return acc; + }); + } + const bg = readCascadeBackgroundColor(current, style, customPropMap); + if (bg && bg.a > 0.1) { + // An opaque surface above the gradient means the gradient never shows + // through here; resolveBackground would have returned it, so reaching + // this is defensive — bail rather than measure the wrong layer. + if (bg.a >= 0.99) return null; + overlays.push(bg); + } current = current.parentElement; } return null; @@ -2798,11 +2856,13 @@ function checkElementGlowDOM(el) { // If resolveBackground returns null (gradient), try to infer from the gradient colors let parentBg = el.parentElement ? resolveBackground(el.parentElement) : resolveBackground(el); if (!parentBg) { - // Gradient background — sample its colors to determine if it's dark + // Gradient background — sample its colors to determine if it's dark. + // Modern-syntax parsing matters here: body-level gradients now reach this + // fallback in browser mode, and their stops usually serialize as oklch. let cur = el.parentElement; while (cur && cur.nodeType === 1) { const bgImage = getComputedStyle(cur).backgroundImage || ''; - const gradColors = parseGradientColors(bgImage); + const gradColors = parseGradientColorsModern(bgImage); if (gradColors.length > 0) { // Average the gradient colors const avg = { r: 0, g: 0, b: 0 }; diff --git a/tests/detect-antipatterns-browser.test.mjs b/tests/detect-antipatterns-browser.test.mjs index c0ef5a24f..40f370231 100644 --- a/tests/detect-antipatterns-browser.test.mjs +++ b/tests/detect-antipatterns-browser.test.mjs @@ -112,6 +112,31 @@ describe('detectUrl — browser-only fixtures', () => { } }); + it('low-contrast: a gradient body ground with oklch stops is measured, never assumed white', async () => { + // The impeccable.style FP class: `background: linear-gradient(oklch(7%…), + // oklch(4%…))` on body leaves backgroundColor transparent, and the old + // resolveBackground assumed white for any body-level gradient — turning + // every light-on-dark text on the page into a ~1.3:1 finding (~120 of + // them on one site). In a real browser, reaching that branch means the + // ground truly is the gradient, so its stops are the surface to measure. + // visualContrast: false scopes this to the DOM resolution path under test; + // the screenshot sampler is a separate subsystem with its own coverage. + const f = await detectUrl(`${baseUrl}/fixtures/antipatterns/dark-gradient-ground.html`, { visualContrast: false }); + const contrast = f.filter(r => r.antipattern === 'low-contrast'); + const snippets = contrast.map(r => r.snippet || '').join('\n'); + assert.doesNotMatch(snippets, /on #ffffff/, `light-on-dark text was measured against an assumed white body:\n${snippets}`); + // Each FLAG case is pinned to its full text-on-background signature (the + // hexes are the engine's own deterministic oklch conversions), so an + // offsetting miss and false positive cannot cancel out — in particular + // the frosted pair: flag-light-on-frosted must be measured against the + // COMPOSITED wash (#dcdbd8), never a raw dark stop, while count === 3 + // proves no pass-column case (like pass-dark-on-frosted) flags instead. + assert.match(snippets, /text #2e2e2e on #010101/, `flag-muted-direct missing against the darker stop:\n${snippets}`); + assert.match(snippets, /text #333333 on #010101/, `flag-muted-nested missing against the darker stop:\n${snippets}`); + assert.match(snippets, /text #d7d7d7 on #dcdbd8/, `flag-light-on-frosted missing against the composited wash:\n${snippets}`); + assert.equal(contrast.length, 3, `expected exactly the 3 flag-column cases, got ${contrast.length}:\n${snippets}`); + }); + it('shadowed form.id: a
with does not crash the scan (issue #407)', async () => { // HTMLFormElement named-property shadowing makes form.id / form.className // return the child input element, whose .startsWith throws. Every Shopify diff --git a/tests/detect-antipatterns-fixtures.test.mjs b/tests/detect-antipatterns-fixtures.test.mjs index 6492b1976..0292d8166 100644 --- a/tests/detect-antipatterns-fixtures.test.mjs +++ b/tests/detect-antipatterns-fixtures.test.mjs @@ -239,6 +239,23 @@ describe('detectHtml — static HTML/CSS fixtures', () => { ); }); + it('dark-gradient-ground: a gradient body ground is measured against its stops, never assumed white', async () => { + // Static-engine twin of the browser test: the page ground is a dark oklch + // gradient set via `background:` shorthand on body (backgroundColor stays + // transparent). The old walk assumed white for any body-level gradient, + // flagging every light text at ~1.3:1 "on #ffffff" and missing the muted + // dark-gray true positives entirely. Stops must be measured instead, and + // the frosted translucent wash must composite over them. + const f = await detectHtml(path.join(FIXTURES, 'dark-gradient-ground.html')); + const contrast = f.filter(r => r.antipattern === 'low-contrast'); + const snippets = contrast.map(r => r.snippet || '').join('\n'); + assert.doesNotMatch(snippets, /on #ffffff/, `light-on-dark text was measured against an assumed white body:\n${snippets}`); + assert.match(snippets, /text #2e2e2e on /, `flag-muted-direct missing:\n${snippets}`); + assert.match(snippets, /text #333333 on /, `flag-muted-nested missing:\n${snippets}`); + assert.match(snippets, /text #d7d7d7 on #d/, `flag-light-on-frosted missing against the composited wash:\n${snippets}`); + assert.equal(contrast.length, 3, `expected exactly the 3 flag-column cases, got ${contrast.length}:\n${snippets}`); + }); + it('color: styled and