diff --git a/.agents/skills/impeccable/SKILL.md b/.agents/skills/impeccable/SKILL.md index de54a3ae2..72f07c311 100644 --- a/.agents/skills/impeccable/SKILL.md +++ b/.agents/skills/impeccable/SKILL.md @@ -9,11 +9,11 @@ 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 -1. Run `node .agents/skills/impeccable/scripts/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 .agents/skills/impeccable/scripts/...` command in this skill and its references, and `.agents/skills/impeccable/scripts` 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/.agents/skills/impeccable/agents/impeccable_asset_producer.toml b/.agents/skills/impeccable/agents/impeccable_asset_producer.toml index 073236d42..7cf60a1dd 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 @@ -56,7 +56,7 @@ Ask blockers once, globally. Missing source path/crops or output directory block Codex: the imagegen skill's built-in `image_gen` path is the native tool here; prefer it for generation, editing, and the chroma-key workflow. 7. Remove baked-in UI text, navigation, buttons, body copy, and mock chrome unless the text is part of the asset. 8. Think through the final DOM/CSS representation before generating. If CSS will own radius, clipping, shadows, borders, perspective, responsive cropping, captions, or card frames, do not bake those into the bitmap. -9. Save outputs non-destructively in the requested project directory, and leave the intent with the file: after every generation, run `node {{scripts_path}}/embed-prompt.mjs --prompt ""` so the prompt is embedded in the image itself, because the build thread composes what you made and needs to know what it is looking at, and the embedding survives copies where sidecars get lost. +9. Save outputs non-destructively in the requested project directory, and leave the intent with the file: after every generation, run `node .agents/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` so the prompt is embedded in the image itself, because the build thread composes what you made and needs to know what it is looking at, and the embedding survives copies where sidecars get lost. 10. Compare each output against its source crop, opening every image by its workspace-relative path; sandboxed viewers reject absolute paths. If a review/QA tool is available, run it before the final manifest, then retry each major/fatal finding once before finalizing. Use `texture/pattern extraction` only when the source region is already clean enough to sample as texture. If UI, cards, labels, headings, body copy, or footer chrome must be removed to make a reusable texture or background, classify it as crop-derived cleanup or clean-plate work. diff --git a/.agents/skills/impeccable/agents/impeccable_finish_reviewer.toml b/.agents/skills/impeccable/agents/impeccable_finish_reviewer.toml index 232fdd5bd..91b45993d 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); 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 -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. @@ -36,5 +36,5 @@ 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/.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/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/.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/craft-floor.md b/.agents/skills/impeccable/reference/craft-floor.md index d9ced54e2..c6f3310f6 100644 --- a/.agents/skills/impeccable/reference/craft-floor.md +++ b/.agents/skills/impeccable/reference/craft-floor.md @@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe - **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows. - **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth. - **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus. +- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably. - **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery. - **Coverage:** every brief requirement present and findable within seconds. 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..e90fd9f20 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); 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 -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. @@ -34,4 +34,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. \ No newline at end of file +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. \ No newline at end of file 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 fa1fb24dd..ca489abf8 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/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. 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,13 +80,13 @@ 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 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. ## 6. Build with full commitment -When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the comp at identical dimensions after every region, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. +When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the freshly reopened comp image at identical dimensions after every region, never beside your memory of it, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. The comp also outranks every written record of it: when the recorded brief or inventory commits to less than the comp shows, a softer texture, a sparser field, a sculpted plate reduced to flat CSS, correct the record upward to the comp; qualifiers like subtle, restrained, and low-contrast, and counts rounded down to a comfortable fraction, are how approved materials die between approval and build. A produced material must then survive to the screen: a texture buried under a nearly opaque color wash ships the wash, not the material, so judge every material by the screenshot beside the comp, never by the stylesheet. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. Build the assigned direction, not a safer interpretation of it. The form supplies structure, reading order, component conventions, and native motion; the product supplies every fact. Commit every atom: nav, buttons, inputs, and links are rebuilt in the form's vocabulary, and a stock component inside a committed form is a lapse. Land the first build fully committed; committing is the hard part, and the passes that follow exist to make the committed thing clear and effective, never to dilute it. In unattended work, the safe rendition is the known risk. @@ -101,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 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. 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/.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/.agents/skills/impeccable/reference/visualize.md b/.agents/skills/impeccable/reference/visualize.md index 815e29d3a..d79c4e8b0 100644 --- a/.agents/skills/impeccable/reference/visualize.md +++ b/.agents/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) 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. ## 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/concept-seed.mjs b/.agents/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..db638ab57 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, @@ -553,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/.agents/skills/impeccable/scripts/context-signals.mjs b/.agents/skills/impeccable/scripts/context-signals.mjs index 743bb220a..e56214be1 100644 --- a/.agents/skills/impeccable/scripts/context-signals.mjs +++ b/.agents/skills/impeccable/scripts/context-signals.mjs @@ -22,7 +22,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { execFileSync } from 'node:child_process'; import { loadContext, extractPlatform } from './context.mjs'; -import { getCritiqueDir } from './lib/impeccable-paths.mjs'; +import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs'; /** Is there code here at all, or just context files / an empty repo? */ function hasCode(cwd) { @@ -34,23 +34,13 @@ function hasCode(cwd) { } /** - * The most recent critique snapshot across all targets. Filenames are - * timestamp-prefixed (`__.md`), so a lexical sort is chronological. - * Parses the small frontmatter for score + P0/P1 counts. + * Summarize the most recent critique snapshot across all targets. */ function latestCritique(cwd) { try { - const dir = getCritiqueDir(cwd); - if (!fs.existsSync(dir)) return null; - const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort(); - if (!files.length) return null; - const newest = files[files.length - 1]; - const text = fs.readFileSync(path.join(dir, newest), 'utf-8'); - const front = text.split('---')[1] || ''; - const get = (k) => { - const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm')); - return m ? m[1].trim() : null; - }; + const latest = readLatestSnapshotAcrossTargets({ cwd }); + if (!latest) return null; + const get = (key) => latest.meta[key] ?? null; const num = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null; @@ -61,7 +51,7 @@ function latestCritique(cwd) { p0: num(get('p0')), p1: num(get('p1')), timestamp: get('timestamp'), - file: path.relative(cwd, path.join(dir, newest)), + file: path.relative(cwd, latest.path), }; } catch { return null; diff --git a/.agents/skills/impeccable/scripts/critique-storage.mjs b/.agents/skills/impeccable/scripts/critique-storage.mjs index a8b36b025..f23fded37 100644 --- a/.agents/skills/impeccable/scripts/critique-storage.mjs +++ b/.agents/skills/impeccable/scripts/critique-storage.mjs @@ -105,28 +105,37 @@ function parseFrontmatter(text) { } /** - * Return all snapshot files for `slug`, sorted oldest → newest. + * Return snapshot files matching `suffix`, sorted oldest → newest. */ -function listSnapshotsForSlug(slug, cwd) { +const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/; + +function listSnapshots(suffix, cwd) { const dir = getCritiqueDir(cwd); if (!fs.existsSync(dir)) return []; - const suffix = `__${slug}.md`; return fs.readdirSync(dir) - .filter((f) => f.endsWith(suffix)) + .filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix)) .sort() .map((f) => path.join(dir, f)); } +function readLatestSnapshotMatching(suffix, cwd) { + const filePath = listSnapshots(suffix, cwd).at(-1); + if (!filePath) return null; + const body = fs.readFileSync(filePath, 'utf-8'); + return { path: filePath, body, meta: parseFrontmatter(body) }; +} + /** * Return the most recent snapshot for `slug`, or null. Polish reads this * to find its fix backlog when the slug matches. */ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); - if (!all.length) return null; - const latest = all[all.length - 1]; - const body = fs.readFileSync(latest, 'utf-8'); - return { path: latest, body, meta: parseFrontmatter(body) }; + return readLatestSnapshotMatching(`__${slug}.md`, cwd); +} + +/** Return the most recent snapshot across all targets, or null. */ +export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) { + return readLatestSnapshotMatching('.md', cwd); } /** @@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { * Critique appends a one-line trend to its output using this. */ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); + const all = listSnapshots(`__${slug}.md`, cwd); const slice = all.slice(-limit); return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8'))); } diff --git a/.agents/skills/impeccable/scripts/detector/detect-antipatterns.mjs b/.agents/skills/impeccable/scripts/detector/detect-antipatterns.mjs index c5bcf064c..e88397e37 100644 --- a/.agents/skills/impeccable/scripts/detector/detect-antipatterns.mjs +++ b/.agents/skills/impeccable/scripts/detector/detect-antipatterns.mjs @@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs'; export { walkDir, + hasScannableExtension, SCANNABLE_EXTENSIONS, SKIP_DIRS, buildImportGraph, diff --git a/.agents/skills/impeccable/scripts/detector/node/file-system.mjs b/.agents/skills/impeccable/scripts/detector/node/file-system.mjs index 6a74fa353..964f6712d 100644 --- a/.agents/skills/impeccable/scripts/detector/node/file-system.mjs +++ b/.agents/skills/impeccable/scripts/detector/node/file-system.mjs @@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']); const SCANNABLE_EXTENSIONS = new Set([ '.html', '.htm', '.css', '.scss', '.sass', '.less', '.jsx', '.tsx', '.js', '.ts', - '.vue', '.svelte', '.astro', + '.vue', '.svelte', '.astro', '.blade.php', ]); const HTML_EXTENSIONS = new Set(['.html', '.htm']); +function hasScannableExtension(filename) { + const lower = filename.toLowerCase(); + if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true; + for (const ext of SCANNABLE_EXTENSIONS) { + if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true; + } + return false; +} + const IMPORT_SPECIFIER_PATTERNS = [ /import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g, /@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g, @@ -46,7 +55,7 @@ function walkDir(dir) { if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue; const full = path.join(dir, entry.name); if (entry.isDirectory()) files.push(...walkDir(full)); - else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full); + else if (hasScannableExtension(entry.name)) files.push(full); } return files; } @@ -194,6 +203,7 @@ export { SKIP_DIRS, SCANNABLE_EXTENSIONS, HTML_EXTENSIONS, + hasScannableExtension, walkDir, resolveImport, buildImportGraph, 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/concept-catalog.mjs b/.agents/skills/impeccable/scripts/lib/concept-catalog.mjs index 9c20711ef..949594d0d 100644 --- a/.agents/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.agents/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes || concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) { errors.push(`concept ${id} must have exactly three structural tags`); } + // The slop this world in particular is at risk of. Optional, because 541 + // entries predate it and none of them are wrong for lacking it. A world built + // from posters is at risk of shouting and one built from instruments is at + // risk of dead greys; a global detector cannot know which, and the author can. + if (concept?.avoid !== undefined) { + if (!Array.isArray(concept.avoid) + || concept.avoid.length < 2 + || concept.avoid.length > 3 + || concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) { + errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`); + } + } if (!Array.isArray(concept?.system) || concept.system.length !== SYSTEM_PREFIXES.length || concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) { 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/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/open-system-browser.mjs b/.agents/skills/impeccable/scripts/lib/open-system-browser.mjs new file mode 100644 index 000000000..c44cd847a --- /dev/null +++ b/.agents/skills/impeccable/scripts/lib/open-system-browser.mjs @@ -0,0 +1,26 @@ +import { spawn } from 'node:child_process'; + +export function browserOpenCommand(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', +} = {}) { + if (platform === 'darwin') return { command: 'open', args: [url] }; + if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] }; + return { command: 'xdg-open', args: [url] }; +} + +export function openSystemBrowser(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', + spawnImpl = spawn, +} = {}) { + const { command, args } = browserOpenCommand(url, { platform, comspec }); + try { + const child = spawnImpl(command, args, { stdio: 'ignore', detached: true }); + child.on('error', () => {}); + child.unref(); + return true; + } catch { + return false; + } +} diff --git a/.agents/skills/impeccable/scripts/lib/roll-selection.mjs b/.agents/skills/impeccable/scripts/lib/roll-selection.mjs index e3c9efbb8..6fab19396 100644 --- a/.agents/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.agents/skills/impeccable/scripts/lib/roll-selection.mjs @@ -96,31 +96,38 @@ function* rank(items, input, idFor = item => item.id) { .map(entry => entry.item); } -// Two independent exclusions, and either one is enough to hold a world back. -// Rating grades quality: a 3-star earns a second ticket, a 1-star marginal keep -// leaves the pool. Breadth says whether a world can serve an arbitrary build at -// all, so a niche world leaves however good it is, keeping its approval for -// direct briefs. Breadth was split out of rating because the only way to hold a -// narrow world back used to be calling it marginal, which made "excellent but -// narrow" unrecordable and corrupted ratings as a calibration signal. +// Rating sets how many tickets a world holds; breadth decides whether it draws +// at all. A niche world leaves the pool however good it is, keeping its approval +// for direct briefs. Breadth was split out of rating because the only way to +// hold a narrow world back used to be calling it marginal, which made "excellent +// but narrow" unrecordable and corrupted ratings as a calibration signal. +// +// Two tickets for a 3-star, one for everything else, was too sharp. Measured +// against the catalog as it stood: 3-star worlds absorbed 57% of the graphic +// draw from 65 of 163 eligible worlds, 46% of atmosphere from 13 of 43, and +// 75% of interaction from 15 of 25. The reviewer's complaint, that the same +// worlds keep coming back, is what a rating multiplier does to a pool whose +// thinnest tier holds 25 worlds. +// +// So a 3-star no longer outdraws a 2-star, and a 1-star draws at half rather +// than not at all. A marginal keep is still worth showing sometimes: the +// judgement it records is "narrow or unexceptional", not "wrong", and excluding +// it entirely made a rating do a job breadth already does properly. +const RATING_TICKETS = { 1: 1, 2: 2, 3: 2 }; +const ticketsForRating = rating => RATING_TICKETS[rating] ?? 2; + function challengerTickets(pool) { return pool.flatMap(concept => { - const rating = concept.review?.rating; - if (rating === 1 || concept.review?.breadth === 'niche') return []; - return rating === 3 - ? [{ concept, ticket: 0 }, { concept, ticket: 1 }] - : [{ concept, ticket: 0 }]; + if (concept.review?.breadth === 'niche') return []; + return Array.from({ length: ticketsForRating(concept.review?.rating) }, + (_, ticket) => ({ concept, ticket })); }); } function compositionTickets(pool) { - return pool.flatMap(composition => { - const rating = composition.review?.rating; - if (rating === 1) return []; - return rating === 3 - ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] - : [{ composition, ticket: 0 }]; - }); + return pool.flatMap(composition => Array.from( + { length: ticketsForRating(composition.review?.rating) }, + (_, ticket) => ({ composition, ticket }))); } /** 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-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.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/.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 `; } @@ -943,22 +1118,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; } @@ -976,8 +1158,7 @@ server.listen(portArg, '127.0.0.1', () => { console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...'); } if (!hasFlag('no-open')) { - const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'; - try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ } + openSystemBrowser(url); } if (timeoutSec > 0) { setTimeout(() => { 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..d03529403 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); 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 -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. @@ -39,4 +39,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/.claude/skills/impeccable/SKILL.md b/.claude/skills/impeccable/SKILL.md index f89c3f92a..43920ae2b 100644 --- a/.claude/skills/impeccable/SKILL.md +++ b/.claude/skills/impeccable/SKILL.md @@ -15,11 +15,11 @@ 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 -1. Run `node .claude/skills/impeccable/scripts/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 .claude/skills/impeccable/scripts/...` command in this skill and its references, and `.claude/skills/impeccable/scripts` 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/.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/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/.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/craft-floor.md b/.claude/skills/impeccable/reference/craft-floor.md index 408f2912e..93be921db 100644 --- a/.claude/skills/impeccable/reference/craft-floor.md +++ b/.claude/skills/impeccable/reference/craft-floor.md @@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe - **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows. - **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth. - **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus. +- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably. - **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery. - **Coverage:** every brief requirement present and findable within seconds. 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..e90fd9f20 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); 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 -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. @@ -34,4 +34,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. \ No newline at end of file +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. \ No newline at end of file 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 0ea9a6d21..859b84a6b 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/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. 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,13 +82,13 @@ 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 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. ## 6. Build with full commitment -When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the comp at identical dimensions after every region, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. +When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the freshly reopened comp image at identical dimensions after every region, never beside your memory of it, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. The comp also outranks every written record of it: when the recorded brief or inventory commits to less than the comp shows, a softer texture, a sparser field, a sculpted plate reduced to flat CSS, correct the record upward to the comp; qualifiers like subtle, restrained, and low-contrast, and counts rounded down to a comfortable fraction, are how approved materials die between approval and build. A produced material must then survive to the screen: a texture buried under a nearly opaque color wash ships the wash, not the material, so judge every material by the screenshot beside the comp, never by the stylesheet. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. Build the assigned direction, not a safer interpretation of it. The form supplies structure, reading order, component conventions, and native motion; the product supplies every fact. Commit every atom: nav, buttons, inputs, and links are rebuilt in the form's vocabulary, and a stock component inside a committed form is a lapse. Land the first build fully committed; committing is the hard part, and the passes that follow exist to make the committed thing clear and effective, never to dilute it. In unattended work, the safe rendition is the known risk. @@ -103,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 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. 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/.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/.claude/skills/impeccable/reference/visualize.md b/.claude/skills/impeccable/reference/visualize.md index 94c337f15..4d91330e4 100644 --- a/.claude/skills/impeccable/reference/visualize.md +++ b/.claude/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) 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. ## 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/concept-seed.mjs b/.claude/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..db638ab57 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, @@ -553,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/context-signals.mjs b/.claude/skills/impeccable/scripts/context-signals.mjs index 743bb220a..e56214be1 100644 --- a/.claude/skills/impeccable/scripts/context-signals.mjs +++ b/.claude/skills/impeccable/scripts/context-signals.mjs @@ -22,7 +22,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { execFileSync } from 'node:child_process'; import { loadContext, extractPlatform } from './context.mjs'; -import { getCritiqueDir } from './lib/impeccable-paths.mjs'; +import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs'; /** Is there code here at all, or just context files / an empty repo? */ function hasCode(cwd) { @@ -34,23 +34,13 @@ function hasCode(cwd) { } /** - * The most recent critique snapshot across all targets. Filenames are - * timestamp-prefixed (`__.md`), so a lexical sort is chronological. - * Parses the small frontmatter for score + P0/P1 counts. + * Summarize the most recent critique snapshot across all targets. */ function latestCritique(cwd) { try { - const dir = getCritiqueDir(cwd); - if (!fs.existsSync(dir)) return null; - const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort(); - if (!files.length) return null; - const newest = files[files.length - 1]; - const text = fs.readFileSync(path.join(dir, newest), 'utf-8'); - const front = text.split('---')[1] || ''; - const get = (k) => { - const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm')); - return m ? m[1].trim() : null; - }; + const latest = readLatestSnapshotAcrossTargets({ cwd }); + if (!latest) return null; + const get = (key) => latest.meta[key] ?? null; const num = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null; @@ -61,7 +51,7 @@ function latestCritique(cwd) { p0: num(get('p0')), p1: num(get('p1')), timestamp: get('timestamp'), - file: path.relative(cwd, path.join(dir, newest)), + file: path.relative(cwd, latest.path), }; } catch { return null; diff --git a/.claude/skills/impeccable/scripts/critique-storage.mjs b/.claude/skills/impeccable/scripts/critique-storage.mjs index a8b36b025..f23fded37 100644 --- a/.claude/skills/impeccable/scripts/critique-storage.mjs +++ b/.claude/skills/impeccable/scripts/critique-storage.mjs @@ -105,28 +105,37 @@ function parseFrontmatter(text) { } /** - * Return all snapshot files for `slug`, sorted oldest → newest. + * Return snapshot files matching `suffix`, sorted oldest → newest. */ -function listSnapshotsForSlug(slug, cwd) { +const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/; + +function listSnapshots(suffix, cwd) { const dir = getCritiqueDir(cwd); if (!fs.existsSync(dir)) return []; - const suffix = `__${slug}.md`; return fs.readdirSync(dir) - .filter((f) => f.endsWith(suffix)) + .filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix)) .sort() .map((f) => path.join(dir, f)); } +function readLatestSnapshotMatching(suffix, cwd) { + const filePath = listSnapshots(suffix, cwd).at(-1); + if (!filePath) return null; + const body = fs.readFileSync(filePath, 'utf-8'); + return { path: filePath, body, meta: parseFrontmatter(body) }; +} + /** * Return the most recent snapshot for `slug`, or null. Polish reads this * to find its fix backlog when the slug matches. */ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); - if (!all.length) return null; - const latest = all[all.length - 1]; - const body = fs.readFileSync(latest, 'utf-8'); - return { path: latest, body, meta: parseFrontmatter(body) }; + return readLatestSnapshotMatching(`__${slug}.md`, cwd); +} + +/** Return the most recent snapshot across all targets, or null. */ +export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) { + return readLatestSnapshotMatching('.md', cwd); } /** @@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { * Critique appends a one-line trend to its output using this. */ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); + const all = listSnapshots(`__${slug}.md`, cwd); const slice = all.slice(-limit); return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8'))); } diff --git a/.claude/skills/impeccable/scripts/detector/detect-antipatterns.mjs b/.claude/skills/impeccable/scripts/detector/detect-antipatterns.mjs index c5bcf064c..e88397e37 100644 --- a/.claude/skills/impeccable/scripts/detector/detect-antipatterns.mjs +++ b/.claude/skills/impeccable/scripts/detector/detect-antipatterns.mjs @@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs'; export { walkDir, + hasScannableExtension, SCANNABLE_EXTENSIONS, SKIP_DIRS, buildImportGraph, diff --git a/.claude/skills/impeccable/scripts/detector/node/file-system.mjs b/.claude/skills/impeccable/scripts/detector/node/file-system.mjs index 6a74fa353..964f6712d 100644 --- a/.claude/skills/impeccable/scripts/detector/node/file-system.mjs +++ b/.claude/skills/impeccable/scripts/detector/node/file-system.mjs @@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']); const SCANNABLE_EXTENSIONS = new Set([ '.html', '.htm', '.css', '.scss', '.sass', '.less', '.jsx', '.tsx', '.js', '.ts', - '.vue', '.svelte', '.astro', + '.vue', '.svelte', '.astro', '.blade.php', ]); const HTML_EXTENSIONS = new Set(['.html', '.htm']); +function hasScannableExtension(filename) { + const lower = filename.toLowerCase(); + if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true; + for (const ext of SCANNABLE_EXTENSIONS) { + if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true; + } + return false; +} + const IMPORT_SPECIFIER_PATTERNS = [ /import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g, /@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g, @@ -46,7 +55,7 @@ function walkDir(dir) { if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue; const full = path.join(dir, entry.name); if (entry.isDirectory()) files.push(...walkDir(full)); - else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full); + else if (hasScannableExtension(entry.name)) files.push(full); } return files; } @@ -194,6 +203,7 @@ export { SKIP_DIRS, SCANNABLE_EXTENSIONS, HTML_EXTENSIONS, + hasScannableExtension, walkDir, resolveImport, buildImportGraph, 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/concept-catalog.mjs b/.claude/skills/impeccable/scripts/lib/concept-catalog.mjs index 9c20711ef..949594d0d 100644 --- a/.claude/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.claude/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes || concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) { errors.push(`concept ${id} must have exactly three structural tags`); } + // The slop this world in particular is at risk of. Optional, because 541 + // entries predate it and none of them are wrong for lacking it. A world built + // from posters is at risk of shouting and one built from instruments is at + // risk of dead greys; a global detector cannot know which, and the author can. + if (concept?.avoid !== undefined) { + if (!Array.isArray(concept.avoid) + || concept.avoid.length < 2 + || concept.avoid.length > 3 + || concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) { + errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`); + } + } if (!Array.isArray(concept?.system) || concept.system.length !== SYSTEM_PREFIXES.length || concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) { 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/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/open-system-browser.mjs b/.claude/skills/impeccable/scripts/lib/open-system-browser.mjs new file mode 100644 index 000000000..c44cd847a --- /dev/null +++ b/.claude/skills/impeccable/scripts/lib/open-system-browser.mjs @@ -0,0 +1,26 @@ +import { spawn } from 'node:child_process'; + +export function browserOpenCommand(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', +} = {}) { + if (platform === 'darwin') return { command: 'open', args: [url] }; + if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] }; + return { command: 'xdg-open', args: [url] }; +} + +export function openSystemBrowser(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', + spawnImpl = spawn, +} = {}) { + const { command, args } = browserOpenCommand(url, { platform, comspec }); + try { + const child = spawnImpl(command, args, { stdio: 'ignore', detached: true }); + child.on('error', () => {}); + child.unref(); + return true; + } catch { + return false; + } +} diff --git a/.claude/skills/impeccable/scripts/lib/roll-selection.mjs b/.claude/skills/impeccable/scripts/lib/roll-selection.mjs index e3c9efbb8..6fab19396 100644 --- a/.claude/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.claude/skills/impeccable/scripts/lib/roll-selection.mjs @@ -96,31 +96,38 @@ function* rank(items, input, idFor = item => item.id) { .map(entry => entry.item); } -// Two independent exclusions, and either one is enough to hold a world back. -// Rating grades quality: a 3-star earns a second ticket, a 1-star marginal keep -// leaves the pool. Breadth says whether a world can serve an arbitrary build at -// all, so a niche world leaves however good it is, keeping its approval for -// direct briefs. Breadth was split out of rating because the only way to hold a -// narrow world back used to be calling it marginal, which made "excellent but -// narrow" unrecordable and corrupted ratings as a calibration signal. +// Rating sets how many tickets a world holds; breadth decides whether it draws +// at all. A niche world leaves the pool however good it is, keeping its approval +// for direct briefs. Breadth was split out of rating because the only way to +// hold a narrow world back used to be calling it marginal, which made "excellent +// but narrow" unrecordable and corrupted ratings as a calibration signal. +// +// Two tickets for a 3-star, one for everything else, was too sharp. Measured +// against the catalog as it stood: 3-star worlds absorbed 57% of the graphic +// draw from 65 of 163 eligible worlds, 46% of atmosphere from 13 of 43, and +// 75% of interaction from 15 of 25. The reviewer's complaint, that the same +// worlds keep coming back, is what a rating multiplier does to a pool whose +// thinnest tier holds 25 worlds. +// +// So a 3-star no longer outdraws a 2-star, and a 1-star draws at half rather +// than not at all. A marginal keep is still worth showing sometimes: the +// judgement it records is "narrow or unexceptional", not "wrong", and excluding +// it entirely made a rating do a job breadth already does properly. +const RATING_TICKETS = { 1: 1, 2: 2, 3: 2 }; +const ticketsForRating = rating => RATING_TICKETS[rating] ?? 2; + function challengerTickets(pool) { return pool.flatMap(concept => { - const rating = concept.review?.rating; - if (rating === 1 || concept.review?.breadth === 'niche') return []; - return rating === 3 - ? [{ concept, ticket: 0 }, { concept, ticket: 1 }] - : [{ concept, ticket: 0 }]; + if (concept.review?.breadth === 'niche') return []; + return Array.from({ length: ticketsForRating(concept.review?.rating) }, + (_, ticket) => ({ concept, ticket })); }); } function compositionTickets(pool) { - return pool.flatMap(composition => { - const rating = composition.review?.rating; - if (rating === 1) return []; - return rating === 3 - ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] - : [{ composition, ticket: 0 }]; - }); + return pool.flatMap(composition => Array.from( + { length: ticketsForRating(composition.review?.rating) }, + (_, ticket) => ({ composition, ticket }))); } /** 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-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.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/.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 `; } @@ -943,22 +1118,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; } @@ -976,8 +1158,7 @@ server.listen(portArg, '127.0.0.1', () => { console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...'); } if (!hasFlag('no-open')) { - const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'; - try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ } + openSystemBrowser(url); } if (timeoutSec > 0) { setTimeout(() => { 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..544a1e464 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); 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 -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. @@ -38,4 +38,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/.cursor/skills/impeccable/SKILL.md b/.cursor/skills/impeccable/SKILL.md index a3676270a..7584a45ff 100644 --- a/.cursor/skills/impeccable/SKILL.md +++ b/.cursor/skills/impeccable/SKILL.md @@ -10,11 +10,11 @@ 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 -1. Run `node .cursor/skills/impeccable/scripts/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 .cursor/skills/impeccable/scripts/...` command in this skill and its references, and `.cursor/skills/impeccable/scripts` 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/.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/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/.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/craft-floor.md b/.cursor/skills/impeccable/reference/craft-floor.md index 408f2912e..93be921db 100644 --- a/.cursor/skills/impeccable/reference/craft-floor.md +++ b/.cursor/skills/impeccable/reference/craft-floor.md @@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe - **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows. - **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth. - **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus. +- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably. - **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery. - **Coverage:** every brief requirement present and findable within seconds. 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..e90fd9f20 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); 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 -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. @@ -34,4 +34,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. \ No newline at end of file +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. \ No newline at end of file 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 0158637bd..e519eb9cd 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/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. 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,13 +80,13 @@ 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 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. ## 6. Build with full commitment -When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the comp at identical dimensions after every region, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. +When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the freshly reopened comp image at identical dimensions after every region, never beside your memory of it, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. The comp also outranks every written record of it: when the recorded brief or inventory commits to less than the comp shows, a softer texture, a sparser field, a sculpted plate reduced to flat CSS, correct the record upward to the comp; qualifiers like subtle, restrained, and low-contrast, and counts rounded down to a comfortable fraction, are how approved materials die between approval and build. A produced material must then survive to the screen: a texture buried under a nearly opaque color wash ships the wash, not the material, so judge every material by the screenshot beside the comp, never by the stylesheet. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. Build the assigned direction, not a safer interpretation of it. The form supplies structure, reading order, component conventions, and native motion; the product supplies every fact. Commit every atom: nav, buttons, inputs, and links are rebuilt in the form's vocabulary, and a stock component inside a committed form is a lapse. Land the first build fully committed; committing is the hard part, and the passes that follow exist to make the committed thing clear and effective, never to dilute it. In unattended work, the safe rendition is the known risk. @@ -101,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 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. 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/.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/.cursor/skills/impeccable/reference/visualize.md b/.cursor/skills/impeccable/reference/visualize.md index 5877eae67..88a21068e 100644 --- a/.cursor/skills/impeccable/reference/visualize.md +++ b/.cursor/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) 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. ## 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/concept-seed.mjs b/.cursor/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..db638ab57 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, @@ -553,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/context-signals.mjs b/.cursor/skills/impeccable/scripts/context-signals.mjs index 743bb220a..e56214be1 100644 --- a/.cursor/skills/impeccable/scripts/context-signals.mjs +++ b/.cursor/skills/impeccable/scripts/context-signals.mjs @@ -22,7 +22,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { execFileSync } from 'node:child_process'; import { loadContext, extractPlatform } from './context.mjs'; -import { getCritiqueDir } from './lib/impeccable-paths.mjs'; +import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs'; /** Is there code here at all, or just context files / an empty repo? */ function hasCode(cwd) { @@ -34,23 +34,13 @@ function hasCode(cwd) { } /** - * The most recent critique snapshot across all targets. Filenames are - * timestamp-prefixed (`__.md`), so a lexical sort is chronological. - * Parses the small frontmatter for score + P0/P1 counts. + * Summarize the most recent critique snapshot across all targets. */ function latestCritique(cwd) { try { - const dir = getCritiqueDir(cwd); - if (!fs.existsSync(dir)) return null; - const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort(); - if (!files.length) return null; - const newest = files[files.length - 1]; - const text = fs.readFileSync(path.join(dir, newest), 'utf-8'); - const front = text.split('---')[1] || ''; - const get = (k) => { - const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm')); - return m ? m[1].trim() : null; - }; + const latest = readLatestSnapshotAcrossTargets({ cwd }); + if (!latest) return null; + const get = (key) => latest.meta[key] ?? null; const num = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null; @@ -61,7 +51,7 @@ function latestCritique(cwd) { p0: num(get('p0')), p1: num(get('p1')), timestamp: get('timestamp'), - file: path.relative(cwd, path.join(dir, newest)), + file: path.relative(cwd, latest.path), }; } catch { return null; diff --git a/.cursor/skills/impeccable/scripts/critique-storage.mjs b/.cursor/skills/impeccable/scripts/critique-storage.mjs index a8b36b025..f23fded37 100644 --- a/.cursor/skills/impeccable/scripts/critique-storage.mjs +++ b/.cursor/skills/impeccable/scripts/critique-storage.mjs @@ -105,28 +105,37 @@ function parseFrontmatter(text) { } /** - * Return all snapshot files for `slug`, sorted oldest → newest. + * Return snapshot files matching `suffix`, sorted oldest → newest. */ -function listSnapshotsForSlug(slug, cwd) { +const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/; + +function listSnapshots(suffix, cwd) { const dir = getCritiqueDir(cwd); if (!fs.existsSync(dir)) return []; - const suffix = `__${slug}.md`; return fs.readdirSync(dir) - .filter((f) => f.endsWith(suffix)) + .filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix)) .sort() .map((f) => path.join(dir, f)); } +function readLatestSnapshotMatching(suffix, cwd) { + const filePath = listSnapshots(suffix, cwd).at(-1); + if (!filePath) return null; + const body = fs.readFileSync(filePath, 'utf-8'); + return { path: filePath, body, meta: parseFrontmatter(body) }; +} + /** * Return the most recent snapshot for `slug`, or null. Polish reads this * to find its fix backlog when the slug matches. */ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); - if (!all.length) return null; - const latest = all[all.length - 1]; - const body = fs.readFileSync(latest, 'utf-8'); - return { path: latest, body, meta: parseFrontmatter(body) }; + return readLatestSnapshotMatching(`__${slug}.md`, cwd); +} + +/** Return the most recent snapshot across all targets, or null. */ +export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) { + return readLatestSnapshotMatching('.md', cwd); } /** @@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { * Critique appends a one-line trend to its output using this. */ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); + const all = listSnapshots(`__${slug}.md`, cwd); const slice = all.slice(-limit); return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8'))); } diff --git a/.cursor/skills/impeccable/scripts/detector/detect-antipatterns.mjs b/.cursor/skills/impeccable/scripts/detector/detect-antipatterns.mjs index c5bcf064c..e88397e37 100644 --- a/.cursor/skills/impeccable/scripts/detector/detect-antipatterns.mjs +++ b/.cursor/skills/impeccable/scripts/detector/detect-antipatterns.mjs @@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs'; export { walkDir, + hasScannableExtension, SCANNABLE_EXTENSIONS, SKIP_DIRS, buildImportGraph, diff --git a/.cursor/skills/impeccable/scripts/detector/node/file-system.mjs b/.cursor/skills/impeccable/scripts/detector/node/file-system.mjs index 6a74fa353..964f6712d 100644 --- a/.cursor/skills/impeccable/scripts/detector/node/file-system.mjs +++ b/.cursor/skills/impeccable/scripts/detector/node/file-system.mjs @@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']); const SCANNABLE_EXTENSIONS = new Set([ '.html', '.htm', '.css', '.scss', '.sass', '.less', '.jsx', '.tsx', '.js', '.ts', - '.vue', '.svelte', '.astro', + '.vue', '.svelte', '.astro', '.blade.php', ]); const HTML_EXTENSIONS = new Set(['.html', '.htm']); +function hasScannableExtension(filename) { + const lower = filename.toLowerCase(); + if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true; + for (const ext of SCANNABLE_EXTENSIONS) { + if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true; + } + return false; +} + const IMPORT_SPECIFIER_PATTERNS = [ /import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g, /@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g, @@ -46,7 +55,7 @@ function walkDir(dir) { if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue; const full = path.join(dir, entry.name); if (entry.isDirectory()) files.push(...walkDir(full)); - else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full); + else if (hasScannableExtension(entry.name)) files.push(full); } return files; } @@ -194,6 +203,7 @@ export { SKIP_DIRS, SCANNABLE_EXTENSIONS, HTML_EXTENSIONS, + hasScannableExtension, walkDir, resolveImport, buildImportGraph, 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/concept-catalog.mjs b/.cursor/skills/impeccable/scripts/lib/concept-catalog.mjs index 9c20711ef..949594d0d 100644 --- a/.cursor/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.cursor/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes || concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) { errors.push(`concept ${id} must have exactly three structural tags`); } + // The slop this world in particular is at risk of. Optional, because 541 + // entries predate it and none of them are wrong for lacking it. A world built + // from posters is at risk of shouting and one built from instruments is at + // risk of dead greys; a global detector cannot know which, and the author can. + if (concept?.avoid !== undefined) { + if (!Array.isArray(concept.avoid) + || concept.avoid.length < 2 + || concept.avoid.length > 3 + || concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) { + errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`); + } + } if (!Array.isArray(concept?.system) || concept.system.length !== SYSTEM_PREFIXES.length || concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) { 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/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/open-system-browser.mjs b/.cursor/skills/impeccable/scripts/lib/open-system-browser.mjs new file mode 100644 index 000000000..c44cd847a --- /dev/null +++ b/.cursor/skills/impeccable/scripts/lib/open-system-browser.mjs @@ -0,0 +1,26 @@ +import { spawn } from 'node:child_process'; + +export function browserOpenCommand(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', +} = {}) { + if (platform === 'darwin') return { command: 'open', args: [url] }; + if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] }; + return { command: 'xdg-open', args: [url] }; +} + +export function openSystemBrowser(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', + spawnImpl = spawn, +} = {}) { + const { command, args } = browserOpenCommand(url, { platform, comspec }); + try { + const child = spawnImpl(command, args, { stdio: 'ignore', detached: true }); + child.on('error', () => {}); + child.unref(); + return true; + } catch { + return false; + } +} diff --git a/.cursor/skills/impeccable/scripts/lib/roll-selection.mjs b/.cursor/skills/impeccable/scripts/lib/roll-selection.mjs index e3c9efbb8..6fab19396 100644 --- a/.cursor/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.cursor/skills/impeccable/scripts/lib/roll-selection.mjs @@ -96,31 +96,38 @@ function* rank(items, input, idFor = item => item.id) { .map(entry => entry.item); } -// Two independent exclusions, and either one is enough to hold a world back. -// Rating grades quality: a 3-star earns a second ticket, a 1-star marginal keep -// leaves the pool. Breadth says whether a world can serve an arbitrary build at -// all, so a niche world leaves however good it is, keeping its approval for -// direct briefs. Breadth was split out of rating because the only way to hold a -// narrow world back used to be calling it marginal, which made "excellent but -// narrow" unrecordable and corrupted ratings as a calibration signal. +// Rating sets how many tickets a world holds; breadth decides whether it draws +// at all. A niche world leaves the pool however good it is, keeping its approval +// for direct briefs. Breadth was split out of rating because the only way to +// hold a narrow world back used to be calling it marginal, which made "excellent +// but narrow" unrecordable and corrupted ratings as a calibration signal. +// +// Two tickets for a 3-star, one for everything else, was too sharp. Measured +// against the catalog as it stood: 3-star worlds absorbed 57% of the graphic +// draw from 65 of 163 eligible worlds, 46% of atmosphere from 13 of 43, and +// 75% of interaction from 15 of 25. The reviewer's complaint, that the same +// worlds keep coming back, is what a rating multiplier does to a pool whose +// thinnest tier holds 25 worlds. +// +// So a 3-star no longer outdraws a 2-star, and a 1-star draws at half rather +// than not at all. A marginal keep is still worth showing sometimes: the +// judgement it records is "narrow or unexceptional", not "wrong", and excluding +// it entirely made a rating do a job breadth already does properly. +const RATING_TICKETS = { 1: 1, 2: 2, 3: 2 }; +const ticketsForRating = rating => RATING_TICKETS[rating] ?? 2; + function challengerTickets(pool) { return pool.flatMap(concept => { - const rating = concept.review?.rating; - if (rating === 1 || concept.review?.breadth === 'niche') return []; - return rating === 3 - ? [{ concept, ticket: 0 }, { concept, ticket: 1 }] - : [{ concept, ticket: 0 }]; + if (concept.review?.breadth === 'niche') return []; + return Array.from({ length: ticketsForRating(concept.review?.rating) }, + (_, ticket) => ({ concept, ticket })); }); } function compositionTickets(pool) { - return pool.flatMap(composition => { - const rating = composition.review?.rating; - if (rating === 1) return []; - return rating === 3 - ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] - : [{ composition, ticket: 0 }]; - }); + return pool.flatMap(composition => Array.from( + { length: ticketsForRating(composition.review?.rating) }, + (_, ticket) => ({ composition, ticket }))); } /** 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-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.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/.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 `; } @@ -943,22 +1118,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; } @@ -976,8 +1158,7 @@ server.listen(portArg, '127.0.0.1', () => { console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...'); } if (!hasFlag('no-open')) { - const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'; - try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ } + openSystemBrowser(url); } if (timeoutSec > 0) { setTimeout(() => { diff --git a/.gemini/skills/impeccable/SKILL.md b/.gemini/skills/impeccable/SKILL.md index 8c6ec5bd5..62bdd7fc6 100644 --- a/.gemini/skills/impeccable/SKILL.md +++ b/.gemini/skills/impeccable/SKILL.md @@ -9,11 +9,11 @@ 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 -1. Run `node .gemini/skills/impeccable/scripts/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 .gemini/skills/impeccable/scripts/...` command in this skill and its references, and `.gemini/skills/impeccable/scripts` 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/.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/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/.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/craft-floor.md b/.gemini/skills/impeccable/reference/craft-floor.md index 5f298f8f5..87381765f 100644 --- a/.gemini/skills/impeccable/reference/craft-floor.md +++ b/.gemini/skills/impeccable/reference/craft-floor.md @@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe - **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows. - **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth. - **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus. +- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably. - **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery. - **Coverage:** every brief requirement present and findable within seconds. 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..e90fd9f20 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); 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 -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. @@ -34,4 +34,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. \ No newline at end of file +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. \ No newline at end of file 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 a09b2e68a..93840d013 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/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. 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,13 +80,13 @@ 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 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. ## 6. Build with full commitment -When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the comp at identical dimensions after every region, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. +When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the freshly reopened comp image at identical dimensions after every region, never beside your memory of it, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. The comp also outranks every written record of it: when the recorded brief or inventory commits to less than the comp shows, a softer texture, a sparser field, a sculpted plate reduced to flat CSS, correct the record upward to the comp; qualifiers like subtle, restrained, and low-contrast, and counts rounded down to a comfortable fraction, are how approved materials die between approval and build. A produced material must then survive to the screen: a texture buried under a nearly opaque color wash ships the wash, not the material, so judge every material by the screenshot beside the comp, never by the stylesheet. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. Build the assigned direction, not a safer interpretation of it. The form supplies structure, reading order, component conventions, and native motion; the product supplies every fact. Commit every atom: nav, buttons, inputs, and links are rebuilt in the form's vocabulary, and a stock component inside a committed form is a lapse. Land the first build fully committed; committing is the hard part, and the passes that follow exist to make the committed thing clear and effective, never to dilute it. In unattended work, the safe rendition is the known risk. @@ -101,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 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. 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/.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/.gemini/skills/impeccable/reference/visualize.md b/.gemini/skills/impeccable/reference/visualize.md index 6dae2cb0b..c5e5fa5cc 100644 --- a/.gemini/skills/impeccable/reference/visualize.md +++ b/.gemini/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) 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. ## 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/concept-seed.mjs b/.gemini/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..db638ab57 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, @@ -553,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/context-signals.mjs b/.gemini/skills/impeccable/scripts/context-signals.mjs index 743bb220a..e56214be1 100644 --- a/.gemini/skills/impeccable/scripts/context-signals.mjs +++ b/.gemini/skills/impeccable/scripts/context-signals.mjs @@ -22,7 +22,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { execFileSync } from 'node:child_process'; import { loadContext, extractPlatform } from './context.mjs'; -import { getCritiqueDir } from './lib/impeccable-paths.mjs'; +import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs'; /** Is there code here at all, or just context files / an empty repo? */ function hasCode(cwd) { @@ -34,23 +34,13 @@ function hasCode(cwd) { } /** - * The most recent critique snapshot across all targets. Filenames are - * timestamp-prefixed (`__.md`), so a lexical sort is chronological. - * Parses the small frontmatter for score + P0/P1 counts. + * Summarize the most recent critique snapshot across all targets. */ function latestCritique(cwd) { try { - const dir = getCritiqueDir(cwd); - if (!fs.existsSync(dir)) return null; - const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort(); - if (!files.length) return null; - const newest = files[files.length - 1]; - const text = fs.readFileSync(path.join(dir, newest), 'utf-8'); - const front = text.split('---')[1] || ''; - const get = (k) => { - const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm')); - return m ? m[1].trim() : null; - }; + const latest = readLatestSnapshotAcrossTargets({ cwd }); + if (!latest) return null; + const get = (key) => latest.meta[key] ?? null; const num = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null; @@ -61,7 +51,7 @@ function latestCritique(cwd) { p0: num(get('p0')), p1: num(get('p1')), timestamp: get('timestamp'), - file: path.relative(cwd, path.join(dir, newest)), + file: path.relative(cwd, latest.path), }; } catch { return null; diff --git a/.gemini/skills/impeccable/scripts/critique-storage.mjs b/.gemini/skills/impeccable/scripts/critique-storage.mjs index a8b36b025..f23fded37 100644 --- a/.gemini/skills/impeccable/scripts/critique-storage.mjs +++ b/.gemini/skills/impeccable/scripts/critique-storage.mjs @@ -105,28 +105,37 @@ function parseFrontmatter(text) { } /** - * Return all snapshot files for `slug`, sorted oldest → newest. + * Return snapshot files matching `suffix`, sorted oldest → newest. */ -function listSnapshotsForSlug(slug, cwd) { +const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/; + +function listSnapshots(suffix, cwd) { const dir = getCritiqueDir(cwd); if (!fs.existsSync(dir)) return []; - const suffix = `__${slug}.md`; return fs.readdirSync(dir) - .filter((f) => f.endsWith(suffix)) + .filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix)) .sort() .map((f) => path.join(dir, f)); } +function readLatestSnapshotMatching(suffix, cwd) { + const filePath = listSnapshots(suffix, cwd).at(-1); + if (!filePath) return null; + const body = fs.readFileSync(filePath, 'utf-8'); + return { path: filePath, body, meta: parseFrontmatter(body) }; +} + /** * Return the most recent snapshot for `slug`, or null. Polish reads this * to find its fix backlog when the slug matches. */ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); - if (!all.length) return null; - const latest = all[all.length - 1]; - const body = fs.readFileSync(latest, 'utf-8'); - return { path: latest, body, meta: parseFrontmatter(body) }; + return readLatestSnapshotMatching(`__${slug}.md`, cwd); +} + +/** Return the most recent snapshot across all targets, or null. */ +export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) { + return readLatestSnapshotMatching('.md', cwd); } /** @@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { * Critique appends a one-line trend to its output using this. */ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); + const all = listSnapshots(`__${slug}.md`, cwd); const slice = all.slice(-limit); return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8'))); } diff --git a/.gemini/skills/impeccable/scripts/detector/detect-antipatterns.mjs b/.gemini/skills/impeccable/scripts/detector/detect-antipatterns.mjs index c5bcf064c..e88397e37 100644 --- a/.gemini/skills/impeccable/scripts/detector/detect-antipatterns.mjs +++ b/.gemini/skills/impeccable/scripts/detector/detect-antipatterns.mjs @@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs'; export { walkDir, + hasScannableExtension, SCANNABLE_EXTENSIONS, SKIP_DIRS, buildImportGraph, diff --git a/.gemini/skills/impeccable/scripts/detector/node/file-system.mjs b/.gemini/skills/impeccable/scripts/detector/node/file-system.mjs index 6a74fa353..964f6712d 100644 --- a/.gemini/skills/impeccable/scripts/detector/node/file-system.mjs +++ b/.gemini/skills/impeccable/scripts/detector/node/file-system.mjs @@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']); const SCANNABLE_EXTENSIONS = new Set([ '.html', '.htm', '.css', '.scss', '.sass', '.less', '.jsx', '.tsx', '.js', '.ts', - '.vue', '.svelte', '.astro', + '.vue', '.svelte', '.astro', '.blade.php', ]); const HTML_EXTENSIONS = new Set(['.html', '.htm']); +function hasScannableExtension(filename) { + const lower = filename.toLowerCase(); + if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true; + for (const ext of SCANNABLE_EXTENSIONS) { + if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true; + } + return false; +} + const IMPORT_SPECIFIER_PATTERNS = [ /import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g, /@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g, @@ -46,7 +55,7 @@ function walkDir(dir) { if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue; const full = path.join(dir, entry.name); if (entry.isDirectory()) files.push(...walkDir(full)); - else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full); + else if (hasScannableExtension(entry.name)) files.push(full); } return files; } @@ -194,6 +203,7 @@ export { SKIP_DIRS, SCANNABLE_EXTENSIONS, HTML_EXTENSIONS, + hasScannableExtension, walkDir, resolveImport, buildImportGraph, 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/concept-catalog.mjs b/.gemini/skills/impeccable/scripts/lib/concept-catalog.mjs index 9c20711ef..949594d0d 100644 --- a/.gemini/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.gemini/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes || concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) { errors.push(`concept ${id} must have exactly three structural tags`); } + // The slop this world in particular is at risk of. Optional, because 541 + // entries predate it and none of them are wrong for lacking it. A world built + // from posters is at risk of shouting and one built from instruments is at + // risk of dead greys; a global detector cannot know which, and the author can. + if (concept?.avoid !== undefined) { + if (!Array.isArray(concept.avoid) + || concept.avoid.length < 2 + || concept.avoid.length > 3 + || concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) { + errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`); + } + } if (!Array.isArray(concept?.system) || concept.system.length !== SYSTEM_PREFIXES.length || concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) { 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/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/open-system-browser.mjs b/.gemini/skills/impeccable/scripts/lib/open-system-browser.mjs new file mode 100644 index 000000000..c44cd847a --- /dev/null +++ b/.gemini/skills/impeccable/scripts/lib/open-system-browser.mjs @@ -0,0 +1,26 @@ +import { spawn } from 'node:child_process'; + +export function browserOpenCommand(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', +} = {}) { + if (platform === 'darwin') return { command: 'open', args: [url] }; + if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] }; + return { command: 'xdg-open', args: [url] }; +} + +export function openSystemBrowser(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', + spawnImpl = spawn, +} = {}) { + const { command, args } = browserOpenCommand(url, { platform, comspec }); + try { + const child = spawnImpl(command, args, { stdio: 'ignore', detached: true }); + child.on('error', () => {}); + child.unref(); + return true; + } catch { + return false; + } +} diff --git a/.gemini/skills/impeccable/scripts/lib/roll-selection.mjs b/.gemini/skills/impeccable/scripts/lib/roll-selection.mjs index e3c9efbb8..6fab19396 100644 --- a/.gemini/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.gemini/skills/impeccable/scripts/lib/roll-selection.mjs @@ -96,31 +96,38 @@ function* rank(items, input, idFor = item => item.id) { .map(entry => entry.item); } -// Two independent exclusions, and either one is enough to hold a world back. -// Rating grades quality: a 3-star earns a second ticket, a 1-star marginal keep -// leaves the pool. Breadth says whether a world can serve an arbitrary build at -// all, so a niche world leaves however good it is, keeping its approval for -// direct briefs. Breadth was split out of rating because the only way to hold a -// narrow world back used to be calling it marginal, which made "excellent but -// narrow" unrecordable and corrupted ratings as a calibration signal. +// Rating sets how many tickets a world holds; breadth decides whether it draws +// at all. A niche world leaves the pool however good it is, keeping its approval +// for direct briefs. Breadth was split out of rating because the only way to +// hold a narrow world back used to be calling it marginal, which made "excellent +// but narrow" unrecordable and corrupted ratings as a calibration signal. +// +// Two tickets for a 3-star, one for everything else, was too sharp. Measured +// against the catalog as it stood: 3-star worlds absorbed 57% of the graphic +// draw from 65 of 163 eligible worlds, 46% of atmosphere from 13 of 43, and +// 75% of interaction from 15 of 25. The reviewer's complaint, that the same +// worlds keep coming back, is what a rating multiplier does to a pool whose +// thinnest tier holds 25 worlds. +// +// So a 3-star no longer outdraws a 2-star, and a 1-star draws at half rather +// than not at all. A marginal keep is still worth showing sometimes: the +// judgement it records is "narrow or unexceptional", not "wrong", and excluding +// it entirely made a rating do a job breadth already does properly. +const RATING_TICKETS = { 1: 1, 2: 2, 3: 2 }; +const ticketsForRating = rating => RATING_TICKETS[rating] ?? 2; + function challengerTickets(pool) { return pool.flatMap(concept => { - const rating = concept.review?.rating; - if (rating === 1 || concept.review?.breadth === 'niche') return []; - return rating === 3 - ? [{ concept, ticket: 0 }, { concept, ticket: 1 }] - : [{ concept, ticket: 0 }]; + if (concept.review?.breadth === 'niche') return []; + return Array.from({ length: ticketsForRating(concept.review?.rating) }, + (_, ticket) => ({ concept, ticket })); }); } function compositionTickets(pool) { - return pool.flatMap(composition => { - const rating = composition.review?.rating; - if (rating === 1) return []; - return rating === 3 - ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] - : [{ composition, ticket: 0 }]; - }); + return pool.flatMap(composition => Array.from( + { length: ticketsForRating(composition.review?.rating) }, + (_, ticket) => ({ composition, ticket }))); } /** 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-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.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/.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 `; } @@ -943,22 +1118,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; } @@ -976,8 +1158,7 @@ server.listen(portArg, '127.0.0.1', () => { console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...'); } if (!hasFlag('no-open')) { - const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'; - try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ } + openSystemBrowser(url); } if (timeoutSec > 0) { setTimeout(() => { diff --git a/.github/skills/impeccable/SKILL.md b/.github/skills/impeccable/SKILL.md index aac5c5f38..b53ababd1 100644 --- a/.github/skills/impeccable/SKILL.md +++ b/.github/skills/impeccable/SKILL.md @@ -12,11 +12,11 @@ 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 -1. Run `node .github/skills/impeccable/scripts/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 .github/skills/impeccable/scripts/...` command in this skill and its references, and `.github/skills/impeccable/scripts` 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/.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/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/.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/craft-floor.md b/.github/skills/impeccable/reference/craft-floor.md index 408f2912e..93be921db 100644 --- a/.github/skills/impeccable/reference/craft-floor.md +++ b/.github/skills/impeccable/reference/craft-floor.md @@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe - **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows. - **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth. - **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus. +- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably. - **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery. - **Coverage:** every brief requirement present and findable within seconds. 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..e90fd9f20 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); 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 -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. @@ -34,4 +34,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. \ No newline at end of file +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. \ No newline at end of file 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 a897e92d2..e39008588 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/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. 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,13 +80,13 @@ 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 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. ## 6. Build with full commitment -When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the comp at identical dimensions after every region, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. +When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the freshly reopened comp image at identical dimensions after every region, never beside your memory of it, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. The comp also outranks every written record of it: when the recorded brief or inventory commits to less than the comp shows, a softer texture, a sparser field, a sculpted plate reduced to flat CSS, correct the record upward to the comp; qualifiers like subtle, restrained, and low-contrast, and counts rounded down to a comfortable fraction, are how approved materials die between approval and build. A produced material must then survive to the screen: a texture buried under a nearly opaque color wash ships the wash, not the material, so judge every material by the screenshot beside the comp, never by the stylesheet. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. Build the assigned direction, not a safer interpretation of it. The form supplies structure, reading order, component conventions, and native motion; the product supplies every fact. Commit every atom: nav, buttons, inputs, and links are rebuilt in the form's vocabulary, and a stock component inside a committed form is a lapse. Land the first build fully committed; committing is the hard part, and the passes that follow exist to make the committed thing clear and effective, never to dilute it. In unattended work, the safe rendition is the known risk. @@ -101,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 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. 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/.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/.github/skills/impeccable/reference/visualize.md b/.github/skills/impeccable/reference/visualize.md index 285a4c996..ad538ba5d 100644 --- a/.github/skills/impeccable/reference/visualize.md +++ b/.github/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) 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. ## 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/concept-seed.mjs b/.github/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..db638ab57 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, @@ -553,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/context-signals.mjs b/.github/skills/impeccable/scripts/context-signals.mjs index 743bb220a..e56214be1 100644 --- a/.github/skills/impeccable/scripts/context-signals.mjs +++ b/.github/skills/impeccable/scripts/context-signals.mjs @@ -22,7 +22,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { execFileSync } from 'node:child_process'; import { loadContext, extractPlatform } from './context.mjs'; -import { getCritiqueDir } from './lib/impeccable-paths.mjs'; +import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs'; /** Is there code here at all, or just context files / an empty repo? */ function hasCode(cwd) { @@ -34,23 +34,13 @@ function hasCode(cwd) { } /** - * The most recent critique snapshot across all targets. Filenames are - * timestamp-prefixed (`__.md`), so a lexical sort is chronological. - * Parses the small frontmatter for score + P0/P1 counts. + * Summarize the most recent critique snapshot across all targets. */ function latestCritique(cwd) { try { - const dir = getCritiqueDir(cwd); - if (!fs.existsSync(dir)) return null; - const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort(); - if (!files.length) return null; - const newest = files[files.length - 1]; - const text = fs.readFileSync(path.join(dir, newest), 'utf-8'); - const front = text.split('---')[1] || ''; - const get = (k) => { - const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm')); - return m ? m[1].trim() : null; - }; + const latest = readLatestSnapshotAcrossTargets({ cwd }); + if (!latest) return null; + const get = (key) => latest.meta[key] ?? null; const num = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null; @@ -61,7 +51,7 @@ function latestCritique(cwd) { p0: num(get('p0')), p1: num(get('p1')), timestamp: get('timestamp'), - file: path.relative(cwd, path.join(dir, newest)), + file: path.relative(cwd, latest.path), }; } catch { return null; diff --git a/.github/skills/impeccable/scripts/critique-storage.mjs b/.github/skills/impeccable/scripts/critique-storage.mjs index a8b36b025..f23fded37 100644 --- a/.github/skills/impeccable/scripts/critique-storage.mjs +++ b/.github/skills/impeccable/scripts/critique-storage.mjs @@ -105,28 +105,37 @@ function parseFrontmatter(text) { } /** - * Return all snapshot files for `slug`, sorted oldest → newest. + * Return snapshot files matching `suffix`, sorted oldest → newest. */ -function listSnapshotsForSlug(slug, cwd) { +const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/; + +function listSnapshots(suffix, cwd) { const dir = getCritiqueDir(cwd); if (!fs.existsSync(dir)) return []; - const suffix = `__${slug}.md`; return fs.readdirSync(dir) - .filter((f) => f.endsWith(suffix)) + .filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix)) .sort() .map((f) => path.join(dir, f)); } +function readLatestSnapshotMatching(suffix, cwd) { + const filePath = listSnapshots(suffix, cwd).at(-1); + if (!filePath) return null; + const body = fs.readFileSync(filePath, 'utf-8'); + return { path: filePath, body, meta: parseFrontmatter(body) }; +} + /** * Return the most recent snapshot for `slug`, or null. Polish reads this * to find its fix backlog when the slug matches. */ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); - if (!all.length) return null; - const latest = all[all.length - 1]; - const body = fs.readFileSync(latest, 'utf-8'); - return { path: latest, body, meta: parseFrontmatter(body) }; + return readLatestSnapshotMatching(`__${slug}.md`, cwd); +} + +/** Return the most recent snapshot across all targets, or null. */ +export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) { + return readLatestSnapshotMatching('.md', cwd); } /** @@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { * Critique appends a one-line trend to its output using this. */ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); + const all = listSnapshots(`__${slug}.md`, cwd); const slice = all.slice(-limit); return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8'))); } diff --git a/.github/skills/impeccable/scripts/detector/detect-antipatterns.mjs b/.github/skills/impeccable/scripts/detector/detect-antipatterns.mjs index c5bcf064c..e88397e37 100644 --- a/.github/skills/impeccable/scripts/detector/detect-antipatterns.mjs +++ b/.github/skills/impeccable/scripts/detector/detect-antipatterns.mjs @@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs'; export { walkDir, + hasScannableExtension, SCANNABLE_EXTENSIONS, SKIP_DIRS, buildImportGraph, diff --git a/.github/skills/impeccable/scripts/detector/node/file-system.mjs b/.github/skills/impeccable/scripts/detector/node/file-system.mjs index 6a74fa353..964f6712d 100644 --- a/.github/skills/impeccable/scripts/detector/node/file-system.mjs +++ b/.github/skills/impeccable/scripts/detector/node/file-system.mjs @@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']); const SCANNABLE_EXTENSIONS = new Set([ '.html', '.htm', '.css', '.scss', '.sass', '.less', '.jsx', '.tsx', '.js', '.ts', - '.vue', '.svelte', '.astro', + '.vue', '.svelte', '.astro', '.blade.php', ]); const HTML_EXTENSIONS = new Set(['.html', '.htm']); +function hasScannableExtension(filename) { + const lower = filename.toLowerCase(); + if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true; + for (const ext of SCANNABLE_EXTENSIONS) { + if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true; + } + return false; +} + const IMPORT_SPECIFIER_PATTERNS = [ /import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g, /@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g, @@ -46,7 +55,7 @@ function walkDir(dir) { if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue; const full = path.join(dir, entry.name); if (entry.isDirectory()) files.push(...walkDir(full)); - else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full); + else if (hasScannableExtension(entry.name)) files.push(full); } return files; } @@ -194,6 +203,7 @@ export { SKIP_DIRS, SCANNABLE_EXTENSIONS, HTML_EXTENSIONS, + hasScannableExtension, walkDir, resolveImport, buildImportGraph, 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/concept-catalog.mjs b/.github/skills/impeccable/scripts/lib/concept-catalog.mjs index 9c20711ef..949594d0d 100644 --- a/.github/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.github/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes || concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) { errors.push(`concept ${id} must have exactly three structural tags`); } + // The slop this world in particular is at risk of. Optional, because 541 + // entries predate it and none of them are wrong for lacking it. A world built + // from posters is at risk of shouting and one built from instruments is at + // risk of dead greys; a global detector cannot know which, and the author can. + if (concept?.avoid !== undefined) { + if (!Array.isArray(concept.avoid) + || concept.avoid.length < 2 + || concept.avoid.length > 3 + || concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) { + errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`); + } + } if (!Array.isArray(concept?.system) || concept.system.length !== SYSTEM_PREFIXES.length || concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) { 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/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/open-system-browser.mjs b/.github/skills/impeccable/scripts/lib/open-system-browser.mjs new file mode 100644 index 000000000..c44cd847a --- /dev/null +++ b/.github/skills/impeccable/scripts/lib/open-system-browser.mjs @@ -0,0 +1,26 @@ +import { spawn } from 'node:child_process'; + +export function browserOpenCommand(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', +} = {}) { + if (platform === 'darwin') return { command: 'open', args: [url] }; + if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] }; + return { command: 'xdg-open', args: [url] }; +} + +export function openSystemBrowser(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', + spawnImpl = spawn, +} = {}) { + const { command, args } = browserOpenCommand(url, { platform, comspec }); + try { + const child = spawnImpl(command, args, { stdio: 'ignore', detached: true }); + child.on('error', () => {}); + child.unref(); + return true; + } catch { + return false; + } +} diff --git a/.github/skills/impeccable/scripts/lib/roll-selection.mjs b/.github/skills/impeccable/scripts/lib/roll-selection.mjs index e3c9efbb8..6fab19396 100644 --- a/.github/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.github/skills/impeccable/scripts/lib/roll-selection.mjs @@ -96,31 +96,38 @@ function* rank(items, input, idFor = item => item.id) { .map(entry => entry.item); } -// Two independent exclusions, and either one is enough to hold a world back. -// Rating grades quality: a 3-star earns a second ticket, a 1-star marginal keep -// leaves the pool. Breadth says whether a world can serve an arbitrary build at -// all, so a niche world leaves however good it is, keeping its approval for -// direct briefs. Breadth was split out of rating because the only way to hold a -// narrow world back used to be calling it marginal, which made "excellent but -// narrow" unrecordable and corrupted ratings as a calibration signal. +// Rating sets how many tickets a world holds; breadth decides whether it draws +// at all. A niche world leaves the pool however good it is, keeping its approval +// for direct briefs. Breadth was split out of rating because the only way to +// hold a narrow world back used to be calling it marginal, which made "excellent +// but narrow" unrecordable and corrupted ratings as a calibration signal. +// +// Two tickets for a 3-star, one for everything else, was too sharp. Measured +// against the catalog as it stood: 3-star worlds absorbed 57% of the graphic +// draw from 65 of 163 eligible worlds, 46% of atmosphere from 13 of 43, and +// 75% of interaction from 15 of 25. The reviewer's complaint, that the same +// worlds keep coming back, is what a rating multiplier does to a pool whose +// thinnest tier holds 25 worlds. +// +// So a 3-star no longer outdraws a 2-star, and a 1-star draws at half rather +// than not at all. A marginal keep is still worth showing sometimes: the +// judgement it records is "narrow or unexceptional", not "wrong", and excluding +// it entirely made a rating do a job breadth already does properly. +const RATING_TICKETS = { 1: 1, 2: 2, 3: 2 }; +const ticketsForRating = rating => RATING_TICKETS[rating] ?? 2; + function challengerTickets(pool) { return pool.flatMap(concept => { - const rating = concept.review?.rating; - if (rating === 1 || concept.review?.breadth === 'niche') return []; - return rating === 3 - ? [{ concept, ticket: 0 }, { concept, ticket: 1 }] - : [{ concept, ticket: 0 }]; + if (concept.review?.breadth === 'niche') return []; + return Array.from({ length: ticketsForRating(concept.review?.rating) }, + (_, ticket) => ({ concept, ticket })); }); } function compositionTickets(pool) { - return pool.flatMap(composition => { - const rating = composition.review?.rating; - if (rating === 1) return []; - return rating === 3 - ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] - : [{ composition, ticket: 0 }]; - }); + return pool.flatMap(composition => Array.from( + { length: ticketsForRating(composition.review?.rating) }, + (_, ticket) => ({ composition, ticket }))); } /** 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-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.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/.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 `; } @@ -943,22 +1118,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; } @@ -976,8 +1158,7 @@ server.listen(portArg, '127.0.0.1', () => { console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...'); } if (!hasFlag('no-open')) { - const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'; - try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ } + openSystemBrowser(url); } if (timeoutSec > 0) { setTimeout(() => { 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..d03529403 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); 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 -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. @@ -39,4 +39,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/.grok/skills/impeccable/SKILL.md b/.grok/skills/impeccable/SKILL.md index 6381ee8b4..d0f9c8142 100644 --- a/.grok/skills/impeccable/SKILL.md +++ b/.grok/skills/impeccable/SKILL.md @@ -15,11 +15,11 @@ 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 -1. Run `node .grok/skills/impeccable/scripts/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 .grok/skills/impeccable/scripts/...` command in this skill and its references, and `.grok/skills/impeccable/scripts` 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/.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/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/.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/craft-floor.md b/.grok/skills/impeccable/reference/craft-floor.md index 408f2912e..93be921db 100644 --- a/.grok/skills/impeccable/reference/craft-floor.md +++ b/.grok/skills/impeccable/reference/craft-floor.md @@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe - **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows. - **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth. - **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus. +- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably. - **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery. - **Coverage:** every brief requirement present and findable within seconds. 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..e90fd9f20 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); 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 -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. @@ -34,4 +34,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. \ No newline at end of file +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. \ No newline at end of file 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 98a628227..884db3fd0 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/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. 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,13 +80,13 @@ 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 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. ## 6. Build with full commitment -When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the comp at identical dimensions after every region, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. +When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the freshly reopened comp image at identical dimensions after every region, never beside your memory of it, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. The comp also outranks every written record of it: when the recorded brief or inventory commits to less than the comp shows, a softer texture, a sparser field, a sculpted plate reduced to flat CSS, correct the record upward to the comp; qualifiers like subtle, restrained, and low-contrast, and counts rounded down to a comfortable fraction, are how approved materials die between approval and build. A produced material must then survive to the screen: a texture buried under a nearly opaque color wash ships the wash, not the material, so judge every material by the screenshot beside the comp, never by the stylesheet. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. Build the assigned direction, not a safer interpretation of it. The form supplies structure, reading order, component conventions, and native motion; the product supplies every fact. Commit every atom: nav, buttons, inputs, and links are rebuilt in the form's vocabulary, and a stock component inside a committed form is a lapse. Land the first build fully committed; committing is the hard part, and the passes that follow exist to make the committed thing clear and effective, never to dilute it. In unattended work, the safe rendition is the known risk. @@ -101,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 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. 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/.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/.grok/skills/impeccable/reference/visualize.md b/.grok/skills/impeccable/reference/visualize.md index 4dd8b3525..33790fe43 100644 --- a/.grok/skills/impeccable/reference/visualize.md +++ b/.grok/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) 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. ## 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/concept-seed.mjs b/.grok/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..db638ab57 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, @@ -553,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/context-signals.mjs b/.grok/skills/impeccable/scripts/context-signals.mjs index 743bb220a..e56214be1 100644 --- a/.grok/skills/impeccable/scripts/context-signals.mjs +++ b/.grok/skills/impeccable/scripts/context-signals.mjs @@ -22,7 +22,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { execFileSync } from 'node:child_process'; import { loadContext, extractPlatform } from './context.mjs'; -import { getCritiqueDir } from './lib/impeccable-paths.mjs'; +import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs'; /** Is there code here at all, or just context files / an empty repo? */ function hasCode(cwd) { @@ -34,23 +34,13 @@ function hasCode(cwd) { } /** - * The most recent critique snapshot across all targets. Filenames are - * timestamp-prefixed (`__.md`), so a lexical sort is chronological. - * Parses the small frontmatter for score + P0/P1 counts. + * Summarize the most recent critique snapshot across all targets. */ function latestCritique(cwd) { try { - const dir = getCritiqueDir(cwd); - if (!fs.existsSync(dir)) return null; - const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort(); - if (!files.length) return null; - const newest = files[files.length - 1]; - const text = fs.readFileSync(path.join(dir, newest), 'utf-8'); - const front = text.split('---')[1] || ''; - const get = (k) => { - const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm')); - return m ? m[1].trim() : null; - }; + const latest = readLatestSnapshotAcrossTargets({ cwd }); + if (!latest) return null; + const get = (key) => latest.meta[key] ?? null; const num = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null; @@ -61,7 +51,7 @@ function latestCritique(cwd) { p0: num(get('p0')), p1: num(get('p1')), timestamp: get('timestamp'), - file: path.relative(cwd, path.join(dir, newest)), + file: path.relative(cwd, latest.path), }; } catch { return null; diff --git a/.grok/skills/impeccable/scripts/critique-storage.mjs b/.grok/skills/impeccable/scripts/critique-storage.mjs index a8b36b025..f23fded37 100644 --- a/.grok/skills/impeccable/scripts/critique-storage.mjs +++ b/.grok/skills/impeccable/scripts/critique-storage.mjs @@ -105,28 +105,37 @@ function parseFrontmatter(text) { } /** - * Return all snapshot files for `slug`, sorted oldest → newest. + * Return snapshot files matching `suffix`, sorted oldest → newest. */ -function listSnapshotsForSlug(slug, cwd) { +const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/; + +function listSnapshots(suffix, cwd) { const dir = getCritiqueDir(cwd); if (!fs.existsSync(dir)) return []; - const suffix = `__${slug}.md`; return fs.readdirSync(dir) - .filter((f) => f.endsWith(suffix)) + .filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix)) .sort() .map((f) => path.join(dir, f)); } +function readLatestSnapshotMatching(suffix, cwd) { + const filePath = listSnapshots(suffix, cwd).at(-1); + if (!filePath) return null; + const body = fs.readFileSync(filePath, 'utf-8'); + return { path: filePath, body, meta: parseFrontmatter(body) }; +} + /** * Return the most recent snapshot for `slug`, or null. Polish reads this * to find its fix backlog when the slug matches. */ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); - if (!all.length) return null; - const latest = all[all.length - 1]; - const body = fs.readFileSync(latest, 'utf-8'); - return { path: latest, body, meta: parseFrontmatter(body) }; + return readLatestSnapshotMatching(`__${slug}.md`, cwd); +} + +/** Return the most recent snapshot across all targets, or null. */ +export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) { + return readLatestSnapshotMatching('.md', cwd); } /** @@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { * Critique appends a one-line trend to its output using this. */ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); + const all = listSnapshots(`__${slug}.md`, cwd); const slice = all.slice(-limit); return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8'))); } diff --git a/.grok/skills/impeccable/scripts/detector/detect-antipatterns.mjs b/.grok/skills/impeccable/scripts/detector/detect-antipatterns.mjs index c5bcf064c..e88397e37 100644 --- a/.grok/skills/impeccable/scripts/detector/detect-antipatterns.mjs +++ b/.grok/skills/impeccable/scripts/detector/detect-antipatterns.mjs @@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs'; export { walkDir, + hasScannableExtension, SCANNABLE_EXTENSIONS, SKIP_DIRS, buildImportGraph, diff --git a/.grok/skills/impeccable/scripts/detector/node/file-system.mjs b/.grok/skills/impeccable/scripts/detector/node/file-system.mjs index 6a74fa353..964f6712d 100644 --- a/.grok/skills/impeccable/scripts/detector/node/file-system.mjs +++ b/.grok/skills/impeccable/scripts/detector/node/file-system.mjs @@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']); const SCANNABLE_EXTENSIONS = new Set([ '.html', '.htm', '.css', '.scss', '.sass', '.less', '.jsx', '.tsx', '.js', '.ts', - '.vue', '.svelte', '.astro', + '.vue', '.svelte', '.astro', '.blade.php', ]); const HTML_EXTENSIONS = new Set(['.html', '.htm']); +function hasScannableExtension(filename) { + const lower = filename.toLowerCase(); + if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true; + for (const ext of SCANNABLE_EXTENSIONS) { + if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true; + } + return false; +} + const IMPORT_SPECIFIER_PATTERNS = [ /import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g, /@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g, @@ -46,7 +55,7 @@ function walkDir(dir) { if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue; const full = path.join(dir, entry.name); if (entry.isDirectory()) files.push(...walkDir(full)); - else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full); + else if (hasScannableExtension(entry.name)) files.push(full); } return files; } @@ -194,6 +203,7 @@ export { SKIP_DIRS, SCANNABLE_EXTENSIONS, HTML_EXTENSIONS, + hasScannableExtension, walkDir, resolveImport, buildImportGraph, 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/concept-catalog.mjs b/.grok/skills/impeccable/scripts/lib/concept-catalog.mjs index 9c20711ef..949594d0d 100644 --- a/.grok/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.grok/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes || concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) { errors.push(`concept ${id} must have exactly three structural tags`); } + // The slop this world in particular is at risk of. Optional, because 541 + // entries predate it and none of them are wrong for lacking it. A world built + // from posters is at risk of shouting and one built from instruments is at + // risk of dead greys; a global detector cannot know which, and the author can. + if (concept?.avoid !== undefined) { + if (!Array.isArray(concept.avoid) + || concept.avoid.length < 2 + || concept.avoid.length > 3 + || concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) { + errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`); + } + } if (!Array.isArray(concept?.system) || concept.system.length !== SYSTEM_PREFIXES.length || concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) { 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/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/open-system-browser.mjs b/.grok/skills/impeccable/scripts/lib/open-system-browser.mjs new file mode 100644 index 000000000..c44cd847a --- /dev/null +++ b/.grok/skills/impeccable/scripts/lib/open-system-browser.mjs @@ -0,0 +1,26 @@ +import { spawn } from 'node:child_process'; + +export function browserOpenCommand(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', +} = {}) { + if (platform === 'darwin') return { command: 'open', args: [url] }; + if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] }; + return { command: 'xdg-open', args: [url] }; +} + +export function openSystemBrowser(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', + spawnImpl = spawn, +} = {}) { + const { command, args } = browserOpenCommand(url, { platform, comspec }); + try { + const child = spawnImpl(command, args, { stdio: 'ignore', detached: true }); + child.on('error', () => {}); + child.unref(); + return true; + } catch { + return false; + } +} diff --git a/.grok/skills/impeccable/scripts/lib/roll-selection.mjs b/.grok/skills/impeccable/scripts/lib/roll-selection.mjs index e3c9efbb8..6fab19396 100644 --- a/.grok/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.grok/skills/impeccable/scripts/lib/roll-selection.mjs @@ -96,31 +96,38 @@ function* rank(items, input, idFor = item => item.id) { .map(entry => entry.item); } -// Two independent exclusions, and either one is enough to hold a world back. -// Rating grades quality: a 3-star earns a second ticket, a 1-star marginal keep -// leaves the pool. Breadth says whether a world can serve an arbitrary build at -// all, so a niche world leaves however good it is, keeping its approval for -// direct briefs. Breadth was split out of rating because the only way to hold a -// narrow world back used to be calling it marginal, which made "excellent but -// narrow" unrecordable and corrupted ratings as a calibration signal. +// Rating sets how many tickets a world holds; breadth decides whether it draws +// at all. A niche world leaves the pool however good it is, keeping its approval +// for direct briefs. Breadth was split out of rating because the only way to +// hold a narrow world back used to be calling it marginal, which made "excellent +// but narrow" unrecordable and corrupted ratings as a calibration signal. +// +// Two tickets for a 3-star, one for everything else, was too sharp. Measured +// against the catalog as it stood: 3-star worlds absorbed 57% of the graphic +// draw from 65 of 163 eligible worlds, 46% of atmosphere from 13 of 43, and +// 75% of interaction from 15 of 25. The reviewer's complaint, that the same +// worlds keep coming back, is what a rating multiplier does to a pool whose +// thinnest tier holds 25 worlds. +// +// So a 3-star no longer outdraws a 2-star, and a 1-star draws at half rather +// than not at all. A marginal keep is still worth showing sometimes: the +// judgement it records is "narrow or unexceptional", not "wrong", and excluding +// it entirely made a rating do a job breadth already does properly. +const RATING_TICKETS = { 1: 1, 2: 2, 3: 2 }; +const ticketsForRating = rating => RATING_TICKETS[rating] ?? 2; + function challengerTickets(pool) { return pool.flatMap(concept => { - const rating = concept.review?.rating; - if (rating === 1 || concept.review?.breadth === 'niche') return []; - return rating === 3 - ? [{ concept, ticket: 0 }, { concept, ticket: 1 }] - : [{ concept, ticket: 0 }]; + if (concept.review?.breadth === 'niche') return []; + return Array.from({ length: ticketsForRating(concept.review?.rating) }, + (_, ticket) => ({ concept, ticket })); }); } function compositionTickets(pool) { - return pool.flatMap(composition => { - const rating = composition.review?.rating; - if (rating === 1) return []; - return rating === 3 - ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] - : [{ composition, ticket: 0 }]; - }); + return pool.flatMap(composition => Array.from( + { length: ticketsForRating(composition.review?.rating) }, + (_, ticket) => ({ composition, ticket }))); } /** 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-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.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/.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 `; } @@ -943,22 +1118,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; } @@ -976,8 +1158,7 @@ server.listen(portArg, '127.0.0.1', () => { console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...'); } if (!hasFlag('no-open')) { - const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'; - try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ } + openSystemBrowser(url); } if (timeoutSec > 0) { setTimeout(() => { diff --git a/.kiro/skills/impeccable/SKILL.md b/.kiro/skills/impeccable/SKILL.md index 066a82aa3..0b23316a7 100644 --- a/.kiro/skills/impeccable/SKILL.md +++ b/.kiro/skills/impeccable/SKILL.md @@ -10,11 +10,11 @@ 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 -1. Run `node .kiro/skills/impeccable/scripts/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 .kiro/skills/impeccable/scripts/...` command in this skill and its references, and `.kiro/skills/impeccable/scripts` 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/.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/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/.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/craft-floor.md b/.kiro/skills/impeccable/reference/craft-floor.md index 408f2912e..93be921db 100644 --- a/.kiro/skills/impeccable/reference/craft-floor.md +++ b/.kiro/skills/impeccable/reference/craft-floor.md @@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe - **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows. - **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth. - **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus. +- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably. - **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery. - **Coverage:** every brief requirement present and findable within seconds. 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..e90fd9f20 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); 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 -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. @@ -34,4 +34,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. \ No newline at end of file +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. \ No newline at end of file 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 d716b4359..0b7bd950b 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/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. 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,13 +80,13 @@ 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 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. ## 6. Build with full commitment -When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the comp at identical dimensions after every region, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. +When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the freshly reopened comp image at identical dimensions after every region, never beside your memory of it, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. The comp also outranks every written record of it: when the recorded brief or inventory commits to less than the comp shows, a softer texture, a sparser field, a sculpted plate reduced to flat CSS, correct the record upward to the comp; qualifiers like subtle, restrained, and low-contrast, and counts rounded down to a comfortable fraction, are how approved materials die between approval and build. A produced material must then survive to the screen: a texture buried under a nearly opaque color wash ships the wash, not the material, so judge every material by the screenshot beside the comp, never by the stylesheet. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. Build the assigned direction, not a safer interpretation of it. The form supplies structure, reading order, component conventions, and native motion; the product supplies every fact. Commit every atom: nav, buttons, inputs, and links are rebuilt in the form's vocabulary, and a stock component inside a committed form is a lapse. Land the first build fully committed; committing is the hard part, and the passes that follow exist to make the committed thing clear and effective, never to dilute it. In unattended work, the safe rendition is the known risk. @@ -101,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 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. 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/.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/.kiro/skills/impeccable/reference/visualize.md b/.kiro/skills/impeccable/reference/visualize.md index f298c6c69..d0388bd0a 100644 --- a/.kiro/skills/impeccable/reference/visualize.md +++ b/.kiro/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) 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. ## 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/concept-seed.mjs b/.kiro/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..db638ab57 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, @@ -553,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/context-signals.mjs b/.kiro/skills/impeccable/scripts/context-signals.mjs index 743bb220a..e56214be1 100644 --- a/.kiro/skills/impeccable/scripts/context-signals.mjs +++ b/.kiro/skills/impeccable/scripts/context-signals.mjs @@ -22,7 +22,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { execFileSync } from 'node:child_process'; import { loadContext, extractPlatform } from './context.mjs'; -import { getCritiqueDir } from './lib/impeccable-paths.mjs'; +import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs'; /** Is there code here at all, or just context files / an empty repo? */ function hasCode(cwd) { @@ -34,23 +34,13 @@ function hasCode(cwd) { } /** - * The most recent critique snapshot across all targets. Filenames are - * timestamp-prefixed (`__.md`), so a lexical sort is chronological. - * Parses the small frontmatter for score + P0/P1 counts. + * Summarize the most recent critique snapshot across all targets. */ function latestCritique(cwd) { try { - const dir = getCritiqueDir(cwd); - if (!fs.existsSync(dir)) return null; - const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort(); - if (!files.length) return null; - const newest = files[files.length - 1]; - const text = fs.readFileSync(path.join(dir, newest), 'utf-8'); - const front = text.split('---')[1] || ''; - const get = (k) => { - const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm')); - return m ? m[1].trim() : null; - }; + const latest = readLatestSnapshotAcrossTargets({ cwd }); + if (!latest) return null; + const get = (key) => latest.meta[key] ?? null; const num = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null; @@ -61,7 +51,7 @@ function latestCritique(cwd) { p0: num(get('p0')), p1: num(get('p1')), timestamp: get('timestamp'), - file: path.relative(cwd, path.join(dir, newest)), + file: path.relative(cwd, latest.path), }; } catch { return null; diff --git a/.kiro/skills/impeccable/scripts/critique-storage.mjs b/.kiro/skills/impeccable/scripts/critique-storage.mjs index a8b36b025..f23fded37 100644 --- a/.kiro/skills/impeccable/scripts/critique-storage.mjs +++ b/.kiro/skills/impeccable/scripts/critique-storage.mjs @@ -105,28 +105,37 @@ function parseFrontmatter(text) { } /** - * Return all snapshot files for `slug`, sorted oldest → newest. + * Return snapshot files matching `suffix`, sorted oldest → newest. */ -function listSnapshotsForSlug(slug, cwd) { +const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/; + +function listSnapshots(suffix, cwd) { const dir = getCritiqueDir(cwd); if (!fs.existsSync(dir)) return []; - const suffix = `__${slug}.md`; return fs.readdirSync(dir) - .filter((f) => f.endsWith(suffix)) + .filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix)) .sort() .map((f) => path.join(dir, f)); } +function readLatestSnapshotMatching(suffix, cwd) { + const filePath = listSnapshots(suffix, cwd).at(-1); + if (!filePath) return null; + const body = fs.readFileSync(filePath, 'utf-8'); + return { path: filePath, body, meta: parseFrontmatter(body) }; +} + /** * Return the most recent snapshot for `slug`, or null. Polish reads this * to find its fix backlog when the slug matches. */ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); - if (!all.length) return null; - const latest = all[all.length - 1]; - const body = fs.readFileSync(latest, 'utf-8'); - return { path: latest, body, meta: parseFrontmatter(body) }; + return readLatestSnapshotMatching(`__${slug}.md`, cwd); +} + +/** Return the most recent snapshot across all targets, or null. */ +export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) { + return readLatestSnapshotMatching('.md', cwd); } /** @@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { * Critique appends a one-line trend to its output using this. */ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); + const all = listSnapshots(`__${slug}.md`, cwd); const slice = all.slice(-limit); return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8'))); } diff --git a/.kiro/skills/impeccable/scripts/detector/detect-antipatterns.mjs b/.kiro/skills/impeccable/scripts/detector/detect-antipatterns.mjs index c5bcf064c..e88397e37 100644 --- a/.kiro/skills/impeccable/scripts/detector/detect-antipatterns.mjs +++ b/.kiro/skills/impeccable/scripts/detector/detect-antipatterns.mjs @@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs'; export { walkDir, + hasScannableExtension, SCANNABLE_EXTENSIONS, SKIP_DIRS, buildImportGraph, diff --git a/.kiro/skills/impeccable/scripts/detector/node/file-system.mjs b/.kiro/skills/impeccable/scripts/detector/node/file-system.mjs index 6a74fa353..964f6712d 100644 --- a/.kiro/skills/impeccable/scripts/detector/node/file-system.mjs +++ b/.kiro/skills/impeccable/scripts/detector/node/file-system.mjs @@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']); const SCANNABLE_EXTENSIONS = new Set([ '.html', '.htm', '.css', '.scss', '.sass', '.less', '.jsx', '.tsx', '.js', '.ts', - '.vue', '.svelte', '.astro', + '.vue', '.svelte', '.astro', '.blade.php', ]); const HTML_EXTENSIONS = new Set(['.html', '.htm']); +function hasScannableExtension(filename) { + const lower = filename.toLowerCase(); + if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true; + for (const ext of SCANNABLE_EXTENSIONS) { + if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true; + } + return false; +} + const IMPORT_SPECIFIER_PATTERNS = [ /import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g, /@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g, @@ -46,7 +55,7 @@ function walkDir(dir) { if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue; const full = path.join(dir, entry.name); if (entry.isDirectory()) files.push(...walkDir(full)); - else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full); + else if (hasScannableExtension(entry.name)) files.push(full); } return files; } @@ -194,6 +203,7 @@ export { SKIP_DIRS, SCANNABLE_EXTENSIONS, HTML_EXTENSIONS, + hasScannableExtension, walkDir, resolveImport, buildImportGraph, 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/concept-catalog.mjs b/.kiro/skills/impeccable/scripts/lib/concept-catalog.mjs index 9c20711ef..949594d0d 100644 --- a/.kiro/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.kiro/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes || concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) { errors.push(`concept ${id} must have exactly three structural tags`); } + // The slop this world in particular is at risk of. Optional, because 541 + // entries predate it and none of them are wrong for lacking it. A world built + // from posters is at risk of shouting and one built from instruments is at + // risk of dead greys; a global detector cannot know which, and the author can. + if (concept?.avoid !== undefined) { + if (!Array.isArray(concept.avoid) + || concept.avoid.length < 2 + || concept.avoid.length > 3 + || concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) { + errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`); + } + } if (!Array.isArray(concept?.system) || concept.system.length !== SYSTEM_PREFIXES.length || concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) { 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/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/open-system-browser.mjs b/.kiro/skills/impeccable/scripts/lib/open-system-browser.mjs new file mode 100644 index 000000000..c44cd847a --- /dev/null +++ b/.kiro/skills/impeccable/scripts/lib/open-system-browser.mjs @@ -0,0 +1,26 @@ +import { spawn } from 'node:child_process'; + +export function browserOpenCommand(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', +} = {}) { + if (platform === 'darwin') return { command: 'open', args: [url] }; + if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] }; + return { command: 'xdg-open', args: [url] }; +} + +export function openSystemBrowser(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', + spawnImpl = spawn, +} = {}) { + const { command, args } = browserOpenCommand(url, { platform, comspec }); + try { + const child = spawnImpl(command, args, { stdio: 'ignore', detached: true }); + child.on('error', () => {}); + child.unref(); + return true; + } catch { + return false; + } +} diff --git a/.kiro/skills/impeccable/scripts/lib/roll-selection.mjs b/.kiro/skills/impeccable/scripts/lib/roll-selection.mjs index e3c9efbb8..6fab19396 100644 --- a/.kiro/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.kiro/skills/impeccable/scripts/lib/roll-selection.mjs @@ -96,31 +96,38 @@ function* rank(items, input, idFor = item => item.id) { .map(entry => entry.item); } -// Two independent exclusions, and either one is enough to hold a world back. -// Rating grades quality: a 3-star earns a second ticket, a 1-star marginal keep -// leaves the pool. Breadth says whether a world can serve an arbitrary build at -// all, so a niche world leaves however good it is, keeping its approval for -// direct briefs. Breadth was split out of rating because the only way to hold a -// narrow world back used to be calling it marginal, which made "excellent but -// narrow" unrecordable and corrupted ratings as a calibration signal. +// Rating sets how many tickets a world holds; breadth decides whether it draws +// at all. A niche world leaves the pool however good it is, keeping its approval +// for direct briefs. Breadth was split out of rating because the only way to +// hold a narrow world back used to be calling it marginal, which made "excellent +// but narrow" unrecordable and corrupted ratings as a calibration signal. +// +// Two tickets for a 3-star, one for everything else, was too sharp. Measured +// against the catalog as it stood: 3-star worlds absorbed 57% of the graphic +// draw from 65 of 163 eligible worlds, 46% of atmosphere from 13 of 43, and +// 75% of interaction from 15 of 25. The reviewer's complaint, that the same +// worlds keep coming back, is what a rating multiplier does to a pool whose +// thinnest tier holds 25 worlds. +// +// So a 3-star no longer outdraws a 2-star, and a 1-star draws at half rather +// than not at all. A marginal keep is still worth showing sometimes: the +// judgement it records is "narrow or unexceptional", not "wrong", and excluding +// it entirely made a rating do a job breadth already does properly. +const RATING_TICKETS = { 1: 1, 2: 2, 3: 2 }; +const ticketsForRating = rating => RATING_TICKETS[rating] ?? 2; + function challengerTickets(pool) { return pool.flatMap(concept => { - const rating = concept.review?.rating; - if (rating === 1 || concept.review?.breadth === 'niche') return []; - return rating === 3 - ? [{ concept, ticket: 0 }, { concept, ticket: 1 }] - : [{ concept, ticket: 0 }]; + if (concept.review?.breadth === 'niche') return []; + return Array.from({ length: ticketsForRating(concept.review?.rating) }, + (_, ticket) => ({ concept, ticket })); }); } function compositionTickets(pool) { - return pool.flatMap(composition => { - const rating = composition.review?.rating; - if (rating === 1) return []; - return rating === 3 - ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] - : [{ composition, ticket: 0 }]; - }); + return pool.flatMap(composition => Array.from( + { length: ticketsForRating(composition.review?.rating) }, + (_, ticket) => ({ composition, ticket }))); } /** 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-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.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/.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 `; } @@ -943,22 +1118,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; } @@ -976,8 +1158,7 @@ server.listen(portArg, '127.0.0.1', () => { console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...'); } if (!hasFlag('no-open')) { - const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'; - try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ } + openSystemBrowser(url); } if (timeoutSec > 0) { setTimeout(() => { diff --git a/.opencode/skills/impeccable/SKILL.md b/.opencode/skills/impeccable/SKILL.md index a3c09b266..5c20ef199 100644 --- a/.opencode/skills/impeccable/SKILL.md +++ b/.opencode/skills/impeccable/SKILL.md @@ -15,11 +15,11 @@ 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 -1. Run `node .opencode/skills/impeccable/scripts/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 .opencode/skills/impeccable/scripts/...` command in this skill and its references, and `.opencode/skills/impeccable/scripts` 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/.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/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/.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/craft-floor.md b/.opencode/skills/impeccable/reference/craft-floor.md index 408f2912e..93be921db 100644 --- a/.opencode/skills/impeccable/reference/craft-floor.md +++ b/.opencode/skills/impeccable/reference/craft-floor.md @@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe - **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows. - **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth. - **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus. +- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably. - **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery. - **Coverage:** every brief requirement present and findable within seconds. 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..e90fd9f20 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); 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 -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. @@ -34,4 +34,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. \ No newline at end of file +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. \ No newline at end of file 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 531bd490c..09792494c 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/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. 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,13 +80,13 @@ 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 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. ## 6. Build with full commitment -When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the comp at identical dimensions after every region, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. +When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the freshly reopened comp image at identical dimensions after every region, never beside your memory of it, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. The comp also outranks every written record of it: when the recorded brief or inventory commits to less than the comp shows, a softer texture, a sparser field, a sculpted plate reduced to flat CSS, correct the record upward to the comp; qualifiers like subtle, restrained, and low-contrast, and counts rounded down to a comfortable fraction, are how approved materials die between approval and build. A produced material must then survive to the screen: a texture buried under a nearly opaque color wash ships the wash, not the material, so judge every material by the screenshot beside the comp, never by the stylesheet. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. Build the assigned direction, not a safer interpretation of it. The form supplies structure, reading order, component conventions, and native motion; the product supplies every fact. Commit every atom: nav, buttons, inputs, and links are rebuilt in the form's vocabulary, and a stock component inside a committed form is a lapse. Land the first build fully committed; committing is the hard part, and the passes that follow exist to make the committed thing clear and effective, never to dilute it. In unattended work, the safe rendition is the known risk. @@ -101,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 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. 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/.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/.opencode/skills/impeccable/reference/visualize.md b/.opencode/skills/impeccable/reference/visualize.md index 43aebde6e..4948f5243 100644 --- a/.opencode/skills/impeccable/reference/visualize.md +++ b/.opencode/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) 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. ## 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/concept-seed.mjs b/.opencode/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..db638ab57 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, @@ -553,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/context-signals.mjs b/.opencode/skills/impeccable/scripts/context-signals.mjs index 743bb220a..e56214be1 100644 --- a/.opencode/skills/impeccable/scripts/context-signals.mjs +++ b/.opencode/skills/impeccable/scripts/context-signals.mjs @@ -22,7 +22,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { execFileSync } from 'node:child_process'; import { loadContext, extractPlatform } from './context.mjs'; -import { getCritiqueDir } from './lib/impeccable-paths.mjs'; +import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs'; /** Is there code here at all, or just context files / an empty repo? */ function hasCode(cwd) { @@ -34,23 +34,13 @@ function hasCode(cwd) { } /** - * The most recent critique snapshot across all targets. Filenames are - * timestamp-prefixed (`__.md`), so a lexical sort is chronological. - * Parses the small frontmatter for score + P0/P1 counts. + * Summarize the most recent critique snapshot across all targets. */ function latestCritique(cwd) { try { - const dir = getCritiqueDir(cwd); - if (!fs.existsSync(dir)) return null; - const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort(); - if (!files.length) return null; - const newest = files[files.length - 1]; - const text = fs.readFileSync(path.join(dir, newest), 'utf-8'); - const front = text.split('---')[1] || ''; - const get = (k) => { - const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm')); - return m ? m[1].trim() : null; - }; + const latest = readLatestSnapshotAcrossTargets({ cwd }); + if (!latest) return null; + const get = (key) => latest.meta[key] ?? null; const num = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null; @@ -61,7 +51,7 @@ function latestCritique(cwd) { p0: num(get('p0')), p1: num(get('p1')), timestamp: get('timestamp'), - file: path.relative(cwd, path.join(dir, newest)), + file: path.relative(cwd, latest.path), }; } catch { return null; diff --git a/.opencode/skills/impeccable/scripts/critique-storage.mjs b/.opencode/skills/impeccable/scripts/critique-storage.mjs index a8b36b025..f23fded37 100644 --- a/.opencode/skills/impeccable/scripts/critique-storage.mjs +++ b/.opencode/skills/impeccable/scripts/critique-storage.mjs @@ -105,28 +105,37 @@ function parseFrontmatter(text) { } /** - * Return all snapshot files for `slug`, sorted oldest → newest. + * Return snapshot files matching `suffix`, sorted oldest → newest. */ -function listSnapshotsForSlug(slug, cwd) { +const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/; + +function listSnapshots(suffix, cwd) { const dir = getCritiqueDir(cwd); if (!fs.existsSync(dir)) return []; - const suffix = `__${slug}.md`; return fs.readdirSync(dir) - .filter((f) => f.endsWith(suffix)) + .filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix)) .sort() .map((f) => path.join(dir, f)); } +function readLatestSnapshotMatching(suffix, cwd) { + const filePath = listSnapshots(suffix, cwd).at(-1); + if (!filePath) return null; + const body = fs.readFileSync(filePath, 'utf-8'); + return { path: filePath, body, meta: parseFrontmatter(body) }; +} + /** * Return the most recent snapshot for `slug`, or null. Polish reads this * to find its fix backlog when the slug matches. */ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); - if (!all.length) return null; - const latest = all[all.length - 1]; - const body = fs.readFileSync(latest, 'utf-8'); - return { path: latest, body, meta: parseFrontmatter(body) }; + return readLatestSnapshotMatching(`__${slug}.md`, cwd); +} + +/** Return the most recent snapshot across all targets, or null. */ +export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) { + return readLatestSnapshotMatching('.md', cwd); } /** @@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { * Critique appends a one-line trend to its output using this. */ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); + const all = listSnapshots(`__${slug}.md`, cwd); const slice = all.slice(-limit); return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8'))); } diff --git a/.opencode/skills/impeccable/scripts/detector/detect-antipatterns.mjs b/.opencode/skills/impeccable/scripts/detector/detect-antipatterns.mjs index c5bcf064c..e88397e37 100644 --- a/.opencode/skills/impeccable/scripts/detector/detect-antipatterns.mjs +++ b/.opencode/skills/impeccable/scripts/detector/detect-antipatterns.mjs @@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs'; export { walkDir, + hasScannableExtension, SCANNABLE_EXTENSIONS, SKIP_DIRS, buildImportGraph, diff --git a/.opencode/skills/impeccable/scripts/detector/node/file-system.mjs b/.opencode/skills/impeccable/scripts/detector/node/file-system.mjs index 6a74fa353..964f6712d 100644 --- a/.opencode/skills/impeccable/scripts/detector/node/file-system.mjs +++ b/.opencode/skills/impeccable/scripts/detector/node/file-system.mjs @@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']); const SCANNABLE_EXTENSIONS = new Set([ '.html', '.htm', '.css', '.scss', '.sass', '.less', '.jsx', '.tsx', '.js', '.ts', - '.vue', '.svelte', '.astro', + '.vue', '.svelte', '.astro', '.blade.php', ]); const HTML_EXTENSIONS = new Set(['.html', '.htm']); +function hasScannableExtension(filename) { + const lower = filename.toLowerCase(); + if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true; + for (const ext of SCANNABLE_EXTENSIONS) { + if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true; + } + return false; +} + const IMPORT_SPECIFIER_PATTERNS = [ /import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g, /@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g, @@ -46,7 +55,7 @@ function walkDir(dir) { if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue; const full = path.join(dir, entry.name); if (entry.isDirectory()) files.push(...walkDir(full)); - else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full); + else if (hasScannableExtension(entry.name)) files.push(full); } return files; } @@ -194,6 +203,7 @@ export { SKIP_DIRS, SCANNABLE_EXTENSIONS, HTML_EXTENSIONS, + hasScannableExtension, walkDir, resolveImport, buildImportGraph, 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/concept-catalog.mjs b/.opencode/skills/impeccable/scripts/lib/concept-catalog.mjs index 9c20711ef..949594d0d 100644 --- a/.opencode/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.opencode/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes || concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) { errors.push(`concept ${id} must have exactly three structural tags`); } + // The slop this world in particular is at risk of. Optional, because 541 + // entries predate it and none of them are wrong for lacking it. A world built + // from posters is at risk of shouting and one built from instruments is at + // risk of dead greys; a global detector cannot know which, and the author can. + if (concept?.avoid !== undefined) { + if (!Array.isArray(concept.avoid) + || concept.avoid.length < 2 + || concept.avoid.length > 3 + || concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) { + errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`); + } + } if (!Array.isArray(concept?.system) || concept.system.length !== SYSTEM_PREFIXES.length || concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) { 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/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/open-system-browser.mjs b/.opencode/skills/impeccable/scripts/lib/open-system-browser.mjs new file mode 100644 index 000000000..c44cd847a --- /dev/null +++ b/.opencode/skills/impeccable/scripts/lib/open-system-browser.mjs @@ -0,0 +1,26 @@ +import { spawn } from 'node:child_process'; + +export function browserOpenCommand(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', +} = {}) { + if (platform === 'darwin') return { command: 'open', args: [url] }; + if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] }; + return { command: 'xdg-open', args: [url] }; +} + +export function openSystemBrowser(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', + spawnImpl = spawn, +} = {}) { + const { command, args } = browserOpenCommand(url, { platform, comspec }); + try { + const child = spawnImpl(command, args, { stdio: 'ignore', detached: true }); + child.on('error', () => {}); + child.unref(); + return true; + } catch { + return false; + } +} diff --git a/.opencode/skills/impeccable/scripts/lib/roll-selection.mjs b/.opencode/skills/impeccable/scripts/lib/roll-selection.mjs index e3c9efbb8..6fab19396 100644 --- a/.opencode/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.opencode/skills/impeccable/scripts/lib/roll-selection.mjs @@ -96,31 +96,38 @@ function* rank(items, input, idFor = item => item.id) { .map(entry => entry.item); } -// Two independent exclusions, and either one is enough to hold a world back. -// Rating grades quality: a 3-star earns a second ticket, a 1-star marginal keep -// leaves the pool. Breadth says whether a world can serve an arbitrary build at -// all, so a niche world leaves however good it is, keeping its approval for -// direct briefs. Breadth was split out of rating because the only way to hold a -// narrow world back used to be calling it marginal, which made "excellent but -// narrow" unrecordable and corrupted ratings as a calibration signal. +// Rating sets how many tickets a world holds; breadth decides whether it draws +// at all. A niche world leaves the pool however good it is, keeping its approval +// for direct briefs. Breadth was split out of rating because the only way to +// hold a narrow world back used to be calling it marginal, which made "excellent +// but narrow" unrecordable and corrupted ratings as a calibration signal. +// +// Two tickets for a 3-star, one for everything else, was too sharp. Measured +// against the catalog as it stood: 3-star worlds absorbed 57% of the graphic +// draw from 65 of 163 eligible worlds, 46% of atmosphere from 13 of 43, and +// 75% of interaction from 15 of 25. The reviewer's complaint, that the same +// worlds keep coming back, is what a rating multiplier does to a pool whose +// thinnest tier holds 25 worlds. +// +// So a 3-star no longer outdraws a 2-star, and a 1-star draws at half rather +// than not at all. A marginal keep is still worth showing sometimes: the +// judgement it records is "narrow or unexceptional", not "wrong", and excluding +// it entirely made a rating do a job breadth already does properly. +const RATING_TICKETS = { 1: 1, 2: 2, 3: 2 }; +const ticketsForRating = rating => RATING_TICKETS[rating] ?? 2; + function challengerTickets(pool) { return pool.flatMap(concept => { - const rating = concept.review?.rating; - if (rating === 1 || concept.review?.breadth === 'niche') return []; - return rating === 3 - ? [{ concept, ticket: 0 }, { concept, ticket: 1 }] - : [{ concept, ticket: 0 }]; + if (concept.review?.breadth === 'niche') return []; + return Array.from({ length: ticketsForRating(concept.review?.rating) }, + (_, ticket) => ({ concept, ticket })); }); } function compositionTickets(pool) { - return pool.flatMap(composition => { - const rating = composition.review?.rating; - if (rating === 1) return []; - return rating === 3 - ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] - : [{ composition, ticket: 0 }]; - }); + return pool.flatMap(composition => Array.from( + { length: ticketsForRating(composition.review?.rating) }, + (_, ticket) => ({ composition, ticket }))); } /** 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-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.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/.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 `; } @@ -943,22 +1118,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; } @@ -976,8 +1158,7 @@ server.listen(portArg, '127.0.0.1', () => { console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...'); } if (!hasFlag('no-open')) { - const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'; - try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ } + openSystemBrowser(url); } if (timeoutSec > 0) { setTimeout(() => { diff --git a/.pi/skills/impeccable/SKILL.md b/.pi/skills/impeccable/SKILL.md index 018671694..460996938 100644 --- a/.pi/skills/impeccable/SKILL.md +++ b/.pi/skills/impeccable/SKILL.md @@ -13,11 +13,11 @@ 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 -1. Run `node .pi/skills/impeccable/scripts/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 .pi/skills/impeccable/scripts/...` command in this skill and its references, and `.pi/skills/impeccable/scripts` 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/.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/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/.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/craft-floor.md b/.pi/skills/impeccable/reference/craft-floor.md index 408f2912e..93be921db 100644 --- a/.pi/skills/impeccable/reference/craft-floor.md +++ b/.pi/skills/impeccable/reference/craft-floor.md @@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe - **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows. - **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth. - **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus. +- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably. - **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery. - **Coverage:** every brief requirement present and findable within seconds. 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..e90fd9f20 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); 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 -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. @@ -34,4 +34,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. \ No newline at end of file +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. \ No newline at end of file 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 4964ab781..396dee5fc 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/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. 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,13 +80,13 @@ 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 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. ## 6. Build with full commitment -When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the comp at identical dimensions after every region, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. +When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the freshly reopened comp image at identical dimensions after every region, never beside your memory of it, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. The comp also outranks every written record of it: when the recorded brief or inventory commits to less than the comp shows, a softer texture, a sparser field, a sculpted plate reduced to flat CSS, correct the record upward to the comp; qualifiers like subtle, restrained, and low-contrast, and counts rounded down to a comfortable fraction, are how approved materials die between approval and build. A produced material must then survive to the screen: a texture buried under a nearly opaque color wash ships the wash, not the material, so judge every material by the screenshot beside the comp, never by the stylesheet. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. Build the assigned direction, not a safer interpretation of it. The form supplies structure, reading order, component conventions, and native motion; the product supplies every fact. Commit every atom: nav, buttons, inputs, and links are rebuilt in the form's vocabulary, and a stock component inside a committed form is a lapse. Land the first build fully committed; committing is the hard part, and the passes that follow exist to make the committed thing clear and effective, never to dilute it. In unattended work, the safe rendition is the known risk. @@ -101,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 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. 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/.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/.pi/skills/impeccable/reference/visualize.md b/.pi/skills/impeccable/reference/visualize.md index 0608624d9..bf38bc747 100644 --- a/.pi/skills/impeccable/reference/visualize.md +++ b/.pi/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) 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. ## 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/concept-seed.mjs b/.pi/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..db638ab57 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, @@ -553,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/context-signals.mjs b/.pi/skills/impeccable/scripts/context-signals.mjs index 743bb220a..e56214be1 100644 --- a/.pi/skills/impeccable/scripts/context-signals.mjs +++ b/.pi/skills/impeccable/scripts/context-signals.mjs @@ -22,7 +22,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { execFileSync } from 'node:child_process'; import { loadContext, extractPlatform } from './context.mjs'; -import { getCritiqueDir } from './lib/impeccable-paths.mjs'; +import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs'; /** Is there code here at all, or just context files / an empty repo? */ function hasCode(cwd) { @@ -34,23 +34,13 @@ function hasCode(cwd) { } /** - * The most recent critique snapshot across all targets. Filenames are - * timestamp-prefixed (`__.md`), so a lexical sort is chronological. - * Parses the small frontmatter for score + P0/P1 counts. + * Summarize the most recent critique snapshot across all targets. */ function latestCritique(cwd) { try { - const dir = getCritiqueDir(cwd); - if (!fs.existsSync(dir)) return null; - const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort(); - if (!files.length) return null; - const newest = files[files.length - 1]; - const text = fs.readFileSync(path.join(dir, newest), 'utf-8'); - const front = text.split('---')[1] || ''; - const get = (k) => { - const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm')); - return m ? m[1].trim() : null; - }; + const latest = readLatestSnapshotAcrossTargets({ cwd }); + if (!latest) return null; + const get = (key) => latest.meta[key] ?? null; const num = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null; @@ -61,7 +51,7 @@ function latestCritique(cwd) { p0: num(get('p0')), p1: num(get('p1')), timestamp: get('timestamp'), - file: path.relative(cwd, path.join(dir, newest)), + file: path.relative(cwd, latest.path), }; } catch { return null; diff --git a/.pi/skills/impeccable/scripts/critique-storage.mjs b/.pi/skills/impeccable/scripts/critique-storage.mjs index a8b36b025..f23fded37 100644 --- a/.pi/skills/impeccable/scripts/critique-storage.mjs +++ b/.pi/skills/impeccable/scripts/critique-storage.mjs @@ -105,28 +105,37 @@ function parseFrontmatter(text) { } /** - * Return all snapshot files for `slug`, sorted oldest → newest. + * Return snapshot files matching `suffix`, sorted oldest → newest. */ -function listSnapshotsForSlug(slug, cwd) { +const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/; + +function listSnapshots(suffix, cwd) { const dir = getCritiqueDir(cwd); if (!fs.existsSync(dir)) return []; - const suffix = `__${slug}.md`; return fs.readdirSync(dir) - .filter((f) => f.endsWith(suffix)) + .filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix)) .sort() .map((f) => path.join(dir, f)); } +function readLatestSnapshotMatching(suffix, cwd) { + const filePath = listSnapshots(suffix, cwd).at(-1); + if (!filePath) return null; + const body = fs.readFileSync(filePath, 'utf-8'); + return { path: filePath, body, meta: parseFrontmatter(body) }; +} + /** * Return the most recent snapshot for `slug`, or null. Polish reads this * to find its fix backlog when the slug matches. */ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); - if (!all.length) return null; - const latest = all[all.length - 1]; - const body = fs.readFileSync(latest, 'utf-8'); - return { path: latest, body, meta: parseFrontmatter(body) }; + return readLatestSnapshotMatching(`__${slug}.md`, cwd); +} + +/** Return the most recent snapshot across all targets, or null. */ +export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) { + return readLatestSnapshotMatching('.md', cwd); } /** @@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { * Critique appends a one-line trend to its output using this. */ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); + const all = listSnapshots(`__${slug}.md`, cwd); const slice = all.slice(-limit); return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8'))); } diff --git a/.pi/skills/impeccable/scripts/detector/detect-antipatterns.mjs b/.pi/skills/impeccable/scripts/detector/detect-antipatterns.mjs index c5bcf064c..e88397e37 100644 --- a/.pi/skills/impeccable/scripts/detector/detect-antipatterns.mjs +++ b/.pi/skills/impeccable/scripts/detector/detect-antipatterns.mjs @@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs'; export { walkDir, + hasScannableExtension, SCANNABLE_EXTENSIONS, SKIP_DIRS, buildImportGraph, diff --git a/.pi/skills/impeccable/scripts/detector/node/file-system.mjs b/.pi/skills/impeccable/scripts/detector/node/file-system.mjs index 6a74fa353..964f6712d 100644 --- a/.pi/skills/impeccable/scripts/detector/node/file-system.mjs +++ b/.pi/skills/impeccable/scripts/detector/node/file-system.mjs @@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']); const SCANNABLE_EXTENSIONS = new Set([ '.html', '.htm', '.css', '.scss', '.sass', '.less', '.jsx', '.tsx', '.js', '.ts', - '.vue', '.svelte', '.astro', + '.vue', '.svelte', '.astro', '.blade.php', ]); const HTML_EXTENSIONS = new Set(['.html', '.htm']); +function hasScannableExtension(filename) { + const lower = filename.toLowerCase(); + if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true; + for (const ext of SCANNABLE_EXTENSIONS) { + if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true; + } + return false; +} + const IMPORT_SPECIFIER_PATTERNS = [ /import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g, /@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g, @@ -46,7 +55,7 @@ function walkDir(dir) { if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue; const full = path.join(dir, entry.name); if (entry.isDirectory()) files.push(...walkDir(full)); - else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full); + else if (hasScannableExtension(entry.name)) files.push(full); } return files; } @@ -194,6 +203,7 @@ export { SKIP_DIRS, SCANNABLE_EXTENSIONS, HTML_EXTENSIONS, + hasScannableExtension, walkDir, resolveImport, buildImportGraph, 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/concept-catalog.mjs b/.pi/skills/impeccable/scripts/lib/concept-catalog.mjs index 9c20711ef..949594d0d 100644 --- a/.pi/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.pi/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes || concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) { errors.push(`concept ${id} must have exactly three structural tags`); } + // The slop this world in particular is at risk of. Optional, because 541 + // entries predate it and none of them are wrong for lacking it. A world built + // from posters is at risk of shouting and one built from instruments is at + // risk of dead greys; a global detector cannot know which, and the author can. + if (concept?.avoid !== undefined) { + if (!Array.isArray(concept.avoid) + || concept.avoid.length < 2 + || concept.avoid.length > 3 + || concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) { + errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`); + } + } if (!Array.isArray(concept?.system) || concept.system.length !== SYSTEM_PREFIXES.length || concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) { 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/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/open-system-browser.mjs b/.pi/skills/impeccable/scripts/lib/open-system-browser.mjs new file mode 100644 index 000000000..c44cd847a --- /dev/null +++ b/.pi/skills/impeccable/scripts/lib/open-system-browser.mjs @@ -0,0 +1,26 @@ +import { spawn } from 'node:child_process'; + +export function browserOpenCommand(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', +} = {}) { + if (platform === 'darwin') return { command: 'open', args: [url] }; + if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] }; + return { command: 'xdg-open', args: [url] }; +} + +export function openSystemBrowser(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', + spawnImpl = spawn, +} = {}) { + const { command, args } = browserOpenCommand(url, { platform, comspec }); + try { + const child = spawnImpl(command, args, { stdio: 'ignore', detached: true }); + child.on('error', () => {}); + child.unref(); + return true; + } catch { + return false; + } +} diff --git a/.pi/skills/impeccable/scripts/lib/roll-selection.mjs b/.pi/skills/impeccable/scripts/lib/roll-selection.mjs index e3c9efbb8..6fab19396 100644 --- a/.pi/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.pi/skills/impeccable/scripts/lib/roll-selection.mjs @@ -96,31 +96,38 @@ function* rank(items, input, idFor = item => item.id) { .map(entry => entry.item); } -// Two independent exclusions, and either one is enough to hold a world back. -// Rating grades quality: a 3-star earns a second ticket, a 1-star marginal keep -// leaves the pool. Breadth says whether a world can serve an arbitrary build at -// all, so a niche world leaves however good it is, keeping its approval for -// direct briefs. Breadth was split out of rating because the only way to hold a -// narrow world back used to be calling it marginal, which made "excellent but -// narrow" unrecordable and corrupted ratings as a calibration signal. +// Rating sets how many tickets a world holds; breadth decides whether it draws +// at all. A niche world leaves the pool however good it is, keeping its approval +// for direct briefs. Breadth was split out of rating because the only way to +// hold a narrow world back used to be calling it marginal, which made "excellent +// but narrow" unrecordable and corrupted ratings as a calibration signal. +// +// Two tickets for a 3-star, one for everything else, was too sharp. Measured +// against the catalog as it stood: 3-star worlds absorbed 57% of the graphic +// draw from 65 of 163 eligible worlds, 46% of atmosphere from 13 of 43, and +// 75% of interaction from 15 of 25. The reviewer's complaint, that the same +// worlds keep coming back, is what a rating multiplier does to a pool whose +// thinnest tier holds 25 worlds. +// +// So a 3-star no longer outdraws a 2-star, and a 1-star draws at half rather +// than not at all. A marginal keep is still worth showing sometimes: the +// judgement it records is "narrow or unexceptional", not "wrong", and excluding +// it entirely made a rating do a job breadth already does properly. +const RATING_TICKETS = { 1: 1, 2: 2, 3: 2 }; +const ticketsForRating = rating => RATING_TICKETS[rating] ?? 2; + function challengerTickets(pool) { return pool.flatMap(concept => { - const rating = concept.review?.rating; - if (rating === 1 || concept.review?.breadth === 'niche') return []; - return rating === 3 - ? [{ concept, ticket: 0 }, { concept, ticket: 1 }] - : [{ concept, ticket: 0 }]; + if (concept.review?.breadth === 'niche') return []; + return Array.from({ length: ticketsForRating(concept.review?.rating) }, + (_, ticket) => ({ concept, ticket })); }); } function compositionTickets(pool) { - return pool.flatMap(composition => { - const rating = composition.review?.rating; - if (rating === 1) return []; - return rating === 3 - ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] - : [{ composition, ticket: 0 }]; - }); + return pool.flatMap(composition => Array.from( + { length: ticketsForRating(composition.review?.rating) }, + (_, ticket) => ({ composition, ticket }))); } /** 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-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.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/.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 `; } @@ -943,22 +1118,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; } @@ -976,8 +1158,7 @@ server.listen(portArg, '127.0.0.1', () => { console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...'); } if (!hasFlag('no-open')) { - const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'; - try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ } + openSystemBrowser(url); } if (timeoutSec > 0) { setTimeout(() => { diff --git a/.qoder/skills/impeccable/SKILL.md b/.qoder/skills/impeccable/SKILL.md index cc683fe90..cbef14a60 100644 --- a/.qoder/skills/impeccable/SKILL.md +++ b/.qoder/skills/impeccable/SKILL.md @@ -15,11 +15,11 @@ 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 -1. Run `node .qoder/skills/impeccable/scripts/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 .qoder/skills/impeccable/scripts/...` command in this skill and its references, and `.qoder/skills/impeccable/scripts` 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/.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/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/.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/craft-floor.md b/.qoder/skills/impeccable/reference/craft-floor.md index 408f2912e..93be921db 100644 --- a/.qoder/skills/impeccable/reference/craft-floor.md +++ b/.qoder/skills/impeccable/reference/craft-floor.md @@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe - **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows. - **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth. - **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus. +- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably. - **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery. - **Coverage:** every brief requirement present and findable within seconds. 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..e90fd9f20 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); 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 -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. @@ -34,4 +34,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. \ No newline at end of file +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. \ No newline at end of file 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 fb8ddb923..286b9ad45 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/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. 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,13 +80,13 @@ 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 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. ## 6. Build with full commitment -When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the comp at identical dimensions after every region, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. +When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the freshly reopened comp image at identical dimensions after every region, never beside your memory of it, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. The comp also outranks every written record of it: when the recorded brief or inventory commits to less than the comp shows, a softer texture, a sparser field, a sculpted plate reduced to flat CSS, correct the record upward to the comp; qualifiers like subtle, restrained, and low-contrast, and counts rounded down to a comfortable fraction, are how approved materials die between approval and build. A produced material must then survive to the screen: a texture buried under a nearly opaque color wash ships the wash, not the material, so judge every material by the screenshot beside the comp, never by the stylesheet. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. Build the assigned direction, not a safer interpretation of it. The form supplies structure, reading order, component conventions, and native motion; the product supplies every fact. Commit every atom: nav, buttons, inputs, and links are rebuilt in the form's vocabulary, and a stock component inside a committed form is a lapse. Land the first build fully committed; committing is the hard part, and the passes that follow exist to make the committed thing clear and effective, never to dilute it. In unattended work, the safe rendition is the known risk. @@ -101,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 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. 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/.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/.qoder/skills/impeccable/reference/visualize.md b/.qoder/skills/impeccable/reference/visualize.md index 7ccc0af4c..136a3cae9 100644 --- a/.qoder/skills/impeccable/reference/visualize.md +++ b/.qoder/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) 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. ## 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/concept-seed.mjs b/.qoder/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..db638ab57 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, @@ -553,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/context-signals.mjs b/.qoder/skills/impeccable/scripts/context-signals.mjs index 743bb220a..e56214be1 100644 --- a/.qoder/skills/impeccable/scripts/context-signals.mjs +++ b/.qoder/skills/impeccable/scripts/context-signals.mjs @@ -22,7 +22,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { execFileSync } from 'node:child_process'; import { loadContext, extractPlatform } from './context.mjs'; -import { getCritiqueDir } from './lib/impeccable-paths.mjs'; +import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs'; /** Is there code here at all, or just context files / an empty repo? */ function hasCode(cwd) { @@ -34,23 +34,13 @@ function hasCode(cwd) { } /** - * The most recent critique snapshot across all targets. Filenames are - * timestamp-prefixed (`__.md`), so a lexical sort is chronological. - * Parses the small frontmatter for score + P0/P1 counts. + * Summarize the most recent critique snapshot across all targets. */ function latestCritique(cwd) { try { - const dir = getCritiqueDir(cwd); - if (!fs.existsSync(dir)) return null; - const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort(); - if (!files.length) return null; - const newest = files[files.length - 1]; - const text = fs.readFileSync(path.join(dir, newest), 'utf-8'); - const front = text.split('---')[1] || ''; - const get = (k) => { - const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm')); - return m ? m[1].trim() : null; - }; + const latest = readLatestSnapshotAcrossTargets({ cwd }); + if (!latest) return null; + const get = (key) => latest.meta[key] ?? null; const num = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null; @@ -61,7 +51,7 @@ function latestCritique(cwd) { p0: num(get('p0')), p1: num(get('p1')), timestamp: get('timestamp'), - file: path.relative(cwd, path.join(dir, newest)), + file: path.relative(cwd, latest.path), }; } catch { return null; diff --git a/.qoder/skills/impeccable/scripts/critique-storage.mjs b/.qoder/skills/impeccable/scripts/critique-storage.mjs index a8b36b025..f23fded37 100644 --- a/.qoder/skills/impeccable/scripts/critique-storage.mjs +++ b/.qoder/skills/impeccable/scripts/critique-storage.mjs @@ -105,28 +105,37 @@ function parseFrontmatter(text) { } /** - * Return all snapshot files for `slug`, sorted oldest → newest. + * Return snapshot files matching `suffix`, sorted oldest → newest. */ -function listSnapshotsForSlug(slug, cwd) { +const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/; + +function listSnapshots(suffix, cwd) { const dir = getCritiqueDir(cwd); if (!fs.existsSync(dir)) return []; - const suffix = `__${slug}.md`; return fs.readdirSync(dir) - .filter((f) => f.endsWith(suffix)) + .filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix)) .sort() .map((f) => path.join(dir, f)); } +function readLatestSnapshotMatching(suffix, cwd) { + const filePath = listSnapshots(suffix, cwd).at(-1); + if (!filePath) return null; + const body = fs.readFileSync(filePath, 'utf-8'); + return { path: filePath, body, meta: parseFrontmatter(body) }; +} + /** * Return the most recent snapshot for `slug`, or null. Polish reads this * to find its fix backlog when the slug matches. */ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); - if (!all.length) return null; - const latest = all[all.length - 1]; - const body = fs.readFileSync(latest, 'utf-8'); - return { path: latest, body, meta: parseFrontmatter(body) }; + return readLatestSnapshotMatching(`__${slug}.md`, cwd); +} + +/** Return the most recent snapshot across all targets, or null. */ +export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) { + return readLatestSnapshotMatching('.md', cwd); } /** @@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { * Critique appends a one-line trend to its output using this. */ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); + const all = listSnapshots(`__${slug}.md`, cwd); const slice = all.slice(-limit); return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8'))); } diff --git a/.qoder/skills/impeccable/scripts/detector/detect-antipatterns.mjs b/.qoder/skills/impeccable/scripts/detector/detect-antipatterns.mjs index c5bcf064c..e88397e37 100644 --- a/.qoder/skills/impeccable/scripts/detector/detect-antipatterns.mjs +++ b/.qoder/skills/impeccable/scripts/detector/detect-antipatterns.mjs @@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs'; export { walkDir, + hasScannableExtension, SCANNABLE_EXTENSIONS, SKIP_DIRS, buildImportGraph, diff --git a/.qoder/skills/impeccable/scripts/detector/node/file-system.mjs b/.qoder/skills/impeccable/scripts/detector/node/file-system.mjs index 6a74fa353..964f6712d 100644 --- a/.qoder/skills/impeccable/scripts/detector/node/file-system.mjs +++ b/.qoder/skills/impeccable/scripts/detector/node/file-system.mjs @@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']); const SCANNABLE_EXTENSIONS = new Set([ '.html', '.htm', '.css', '.scss', '.sass', '.less', '.jsx', '.tsx', '.js', '.ts', - '.vue', '.svelte', '.astro', + '.vue', '.svelte', '.astro', '.blade.php', ]); const HTML_EXTENSIONS = new Set(['.html', '.htm']); +function hasScannableExtension(filename) { + const lower = filename.toLowerCase(); + if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true; + for (const ext of SCANNABLE_EXTENSIONS) { + if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true; + } + return false; +} + const IMPORT_SPECIFIER_PATTERNS = [ /import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g, /@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g, @@ -46,7 +55,7 @@ function walkDir(dir) { if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue; const full = path.join(dir, entry.name); if (entry.isDirectory()) files.push(...walkDir(full)); - else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full); + else if (hasScannableExtension(entry.name)) files.push(full); } return files; } @@ -194,6 +203,7 @@ export { SKIP_DIRS, SCANNABLE_EXTENSIONS, HTML_EXTENSIONS, + hasScannableExtension, walkDir, resolveImport, buildImportGraph, 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/concept-catalog.mjs b/.qoder/skills/impeccable/scripts/lib/concept-catalog.mjs index 9c20711ef..949594d0d 100644 --- a/.qoder/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.qoder/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes || concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) { errors.push(`concept ${id} must have exactly three structural tags`); } + // The slop this world in particular is at risk of. Optional, because 541 + // entries predate it and none of them are wrong for lacking it. A world built + // from posters is at risk of shouting and one built from instruments is at + // risk of dead greys; a global detector cannot know which, and the author can. + if (concept?.avoid !== undefined) { + if (!Array.isArray(concept.avoid) + || concept.avoid.length < 2 + || concept.avoid.length > 3 + || concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) { + errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`); + } + } if (!Array.isArray(concept?.system) || concept.system.length !== SYSTEM_PREFIXES.length || concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) { 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/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/open-system-browser.mjs b/.qoder/skills/impeccable/scripts/lib/open-system-browser.mjs new file mode 100644 index 000000000..c44cd847a --- /dev/null +++ b/.qoder/skills/impeccable/scripts/lib/open-system-browser.mjs @@ -0,0 +1,26 @@ +import { spawn } from 'node:child_process'; + +export function browserOpenCommand(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', +} = {}) { + if (platform === 'darwin') return { command: 'open', args: [url] }; + if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] }; + return { command: 'xdg-open', args: [url] }; +} + +export function openSystemBrowser(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', + spawnImpl = spawn, +} = {}) { + const { command, args } = browserOpenCommand(url, { platform, comspec }); + try { + const child = spawnImpl(command, args, { stdio: 'ignore', detached: true }); + child.on('error', () => {}); + child.unref(); + return true; + } catch { + return false; + } +} diff --git a/.qoder/skills/impeccable/scripts/lib/roll-selection.mjs b/.qoder/skills/impeccable/scripts/lib/roll-selection.mjs index e3c9efbb8..6fab19396 100644 --- a/.qoder/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.qoder/skills/impeccable/scripts/lib/roll-selection.mjs @@ -96,31 +96,38 @@ function* rank(items, input, idFor = item => item.id) { .map(entry => entry.item); } -// Two independent exclusions, and either one is enough to hold a world back. -// Rating grades quality: a 3-star earns a second ticket, a 1-star marginal keep -// leaves the pool. Breadth says whether a world can serve an arbitrary build at -// all, so a niche world leaves however good it is, keeping its approval for -// direct briefs. Breadth was split out of rating because the only way to hold a -// narrow world back used to be calling it marginal, which made "excellent but -// narrow" unrecordable and corrupted ratings as a calibration signal. +// Rating sets how many tickets a world holds; breadth decides whether it draws +// at all. A niche world leaves the pool however good it is, keeping its approval +// for direct briefs. Breadth was split out of rating because the only way to +// hold a narrow world back used to be calling it marginal, which made "excellent +// but narrow" unrecordable and corrupted ratings as a calibration signal. +// +// Two tickets for a 3-star, one for everything else, was too sharp. Measured +// against the catalog as it stood: 3-star worlds absorbed 57% of the graphic +// draw from 65 of 163 eligible worlds, 46% of atmosphere from 13 of 43, and +// 75% of interaction from 15 of 25. The reviewer's complaint, that the same +// worlds keep coming back, is what a rating multiplier does to a pool whose +// thinnest tier holds 25 worlds. +// +// So a 3-star no longer outdraws a 2-star, and a 1-star draws at half rather +// than not at all. A marginal keep is still worth showing sometimes: the +// judgement it records is "narrow or unexceptional", not "wrong", and excluding +// it entirely made a rating do a job breadth already does properly. +const RATING_TICKETS = { 1: 1, 2: 2, 3: 2 }; +const ticketsForRating = rating => RATING_TICKETS[rating] ?? 2; + function challengerTickets(pool) { return pool.flatMap(concept => { - const rating = concept.review?.rating; - if (rating === 1 || concept.review?.breadth === 'niche') return []; - return rating === 3 - ? [{ concept, ticket: 0 }, { concept, ticket: 1 }] - : [{ concept, ticket: 0 }]; + if (concept.review?.breadth === 'niche') return []; + return Array.from({ length: ticketsForRating(concept.review?.rating) }, + (_, ticket) => ({ concept, ticket })); }); } function compositionTickets(pool) { - return pool.flatMap(composition => { - const rating = composition.review?.rating; - if (rating === 1) return []; - return rating === 3 - ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] - : [{ composition, ticket: 0 }]; - }); + return pool.flatMap(composition => Array.from( + { length: ticketsForRating(composition.review?.rating) }, + (_, ticket) => ({ composition, ticket }))); } /** 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-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.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/.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 `; } @@ -943,22 +1118,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; } @@ -976,8 +1158,7 @@ server.listen(portArg, '127.0.0.1', () => { console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...'); } if (!hasFlag('no-open')) { - const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'; - try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ } + openSystemBrowser(url); } if (timeoutSec > 0) { setTimeout(() => { diff --git a/.rovodev/skills/impeccable/SKILL.md b/.rovodev/skills/impeccable/SKILL.md index 5a7037414..648660e93 100644 --- a/.rovodev/skills/impeccable/SKILL.md +++ b/.rovodev/skills/impeccable/SKILL.md @@ -15,11 +15,11 @@ 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 -1. Run `node .rovodev/skills/impeccable/scripts/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 .rovodev/skills/impeccable/scripts/...` command in this skill and its references, and `.rovodev/skills/impeccable/scripts` 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/.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/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/.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/craft-floor.md b/.rovodev/skills/impeccable/reference/craft-floor.md index 408f2912e..93be921db 100644 --- a/.rovodev/skills/impeccable/reference/craft-floor.md +++ b/.rovodev/skills/impeccable/reference/craft-floor.md @@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe - **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows. - **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth. - **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus. +- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably. - **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery. - **Coverage:** every brief requirement present and findable within seconds. 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..e90fd9f20 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); 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 -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. @@ -34,4 +34,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. \ No newline at end of file +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. \ No newline at end of file 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 6a62f6715..47f451c6b 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/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. 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,13 +80,13 @@ 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 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. ## 6. Build with full commitment -When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the comp at identical dimensions after every region, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. +When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the freshly reopened comp image at identical dimensions after every region, never beside your memory of it, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. The comp also outranks every written record of it: when the recorded brief or inventory commits to less than the comp shows, a softer texture, a sparser field, a sculpted plate reduced to flat CSS, correct the record upward to the comp; qualifiers like subtle, restrained, and low-contrast, and counts rounded down to a comfortable fraction, are how approved materials die between approval and build. A produced material must then survive to the screen: a texture buried under a nearly opaque color wash ships the wash, not the material, so judge every material by the screenshot beside the comp, never by the stylesheet. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. Build the assigned direction, not a safer interpretation of it. The form supplies structure, reading order, component conventions, and native motion; the product supplies every fact. Commit every atom: nav, buttons, inputs, and links are rebuilt in the form's vocabulary, and a stock component inside a committed form is a lapse. Land the first build fully committed; committing is the hard part, and the passes that follow exist to make the committed thing clear and effective, never to dilute it. In unattended work, the safe rendition is the known risk. @@ -101,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 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. 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/.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/.rovodev/skills/impeccable/reference/visualize.md b/.rovodev/skills/impeccable/reference/visualize.md index ad1f4a548..d7df668e0 100644 --- a/.rovodev/skills/impeccable/reference/visualize.md +++ b/.rovodev/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) 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. ## 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/concept-seed.mjs b/.rovodev/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..db638ab57 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, @@ -553,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/context-signals.mjs b/.rovodev/skills/impeccable/scripts/context-signals.mjs index 743bb220a..e56214be1 100644 --- a/.rovodev/skills/impeccable/scripts/context-signals.mjs +++ b/.rovodev/skills/impeccable/scripts/context-signals.mjs @@ -22,7 +22,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { execFileSync } from 'node:child_process'; import { loadContext, extractPlatform } from './context.mjs'; -import { getCritiqueDir } from './lib/impeccable-paths.mjs'; +import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs'; /** Is there code here at all, or just context files / an empty repo? */ function hasCode(cwd) { @@ -34,23 +34,13 @@ function hasCode(cwd) { } /** - * The most recent critique snapshot across all targets. Filenames are - * timestamp-prefixed (`__.md`), so a lexical sort is chronological. - * Parses the small frontmatter for score + P0/P1 counts. + * Summarize the most recent critique snapshot across all targets. */ function latestCritique(cwd) { try { - const dir = getCritiqueDir(cwd); - if (!fs.existsSync(dir)) return null; - const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort(); - if (!files.length) return null; - const newest = files[files.length - 1]; - const text = fs.readFileSync(path.join(dir, newest), 'utf-8'); - const front = text.split('---')[1] || ''; - const get = (k) => { - const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm')); - return m ? m[1].trim() : null; - }; + const latest = readLatestSnapshotAcrossTargets({ cwd }); + if (!latest) return null; + const get = (key) => latest.meta[key] ?? null; const num = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null; @@ -61,7 +51,7 @@ function latestCritique(cwd) { p0: num(get('p0')), p1: num(get('p1')), timestamp: get('timestamp'), - file: path.relative(cwd, path.join(dir, newest)), + file: path.relative(cwd, latest.path), }; } catch { return null; diff --git a/.rovodev/skills/impeccable/scripts/critique-storage.mjs b/.rovodev/skills/impeccable/scripts/critique-storage.mjs index a8b36b025..f23fded37 100644 --- a/.rovodev/skills/impeccable/scripts/critique-storage.mjs +++ b/.rovodev/skills/impeccable/scripts/critique-storage.mjs @@ -105,28 +105,37 @@ function parseFrontmatter(text) { } /** - * Return all snapshot files for `slug`, sorted oldest → newest. + * Return snapshot files matching `suffix`, sorted oldest → newest. */ -function listSnapshotsForSlug(slug, cwd) { +const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/; + +function listSnapshots(suffix, cwd) { const dir = getCritiqueDir(cwd); if (!fs.existsSync(dir)) return []; - const suffix = `__${slug}.md`; return fs.readdirSync(dir) - .filter((f) => f.endsWith(suffix)) + .filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix)) .sort() .map((f) => path.join(dir, f)); } +function readLatestSnapshotMatching(suffix, cwd) { + const filePath = listSnapshots(suffix, cwd).at(-1); + if (!filePath) return null; + const body = fs.readFileSync(filePath, 'utf-8'); + return { path: filePath, body, meta: parseFrontmatter(body) }; +} + /** * Return the most recent snapshot for `slug`, or null. Polish reads this * to find its fix backlog when the slug matches. */ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); - if (!all.length) return null; - const latest = all[all.length - 1]; - const body = fs.readFileSync(latest, 'utf-8'); - return { path: latest, body, meta: parseFrontmatter(body) }; + return readLatestSnapshotMatching(`__${slug}.md`, cwd); +} + +/** Return the most recent snapshot across all targets, or null. */ +export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) { + return readLatestSnapshotMatching('.md', cwd); } /** @@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { * Critique appends a one-line trend to its output using this. */ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); + const all = listSnapshots(`__${slug}.md`, cwd); const slice = all.slice(-limit); return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8'))); } diff --git a/.rovodev/skills/impeccable/scripts/detector/detect-antipatterns.mjs b/.rovodev/skills/impeccable/scripts/detector/detect-antipatterns.mjs index c5bcf064c..e88397e37 100644 --- a/.rovodev/skills/impeccable/scripts/detector/detect-antipatterns.mjs +++ b/.rovodev/skills/impeccable/scripts/detector/detect-antipatterns.mjs @@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs'; export { walkDir, + hasScannableExtension, SCANNABLE_EXTENSIONS, SKIP_DIRS, buildImportGraph, diff --git a/.rovodev/skills/impeccable/scripts/detector/node/file-system.mjs b/.rovodev/skills/impeccable/scripts/detector/node/file-system.mjs index 6a74fa353..964f6712d 100644 --- a/.rovodev/skills/impeccable/scripts/detector/node/file-system.mjs +++ b/.rovodev/skills/impeccable/scripts/detector/node/file-system.mjs @@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']); const SCANNABLE_EXTENSIONS = new Set([ '.html', '.htm', '.css', '.scss', '.sass', '.less', '.jsx', '.tsx', '.js', '.ts', - '.vue', '.svelte', '.astro', + '.vue', '.svelte', '.astro', '.blade.php', ]); const HTML_EXTENSIONS = new Set(['.html', '.htm']); +function hasScannableExtension(filename) { + const lower = filename.toLowerCase(); + if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true; + for (const ext of SCANNABLE_EXTENSIONS) { + if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true; + } + return false; +} + const IMPORT_SPECIFIER_PATTERNS = [ /import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g, /@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g, @@ -46,7 +55,7 @@ function walkDir(dir) { if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue; const full = path.join(dir, entry.name); if (entry.isDirectory()) files.push(...walkDir(full)); - else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full); + else if (hasScannableExtension(entry.name)) files.push(full); } return files; } @@ -194,6 +203,7 @@ export { SKIP_DIRS, SCANNABLE_EXTENSIONS, HTML_EXTENSIONS, + hasScannableExtension, walkDir, resolveImport, buildImportGraph, 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/concept-catalog.mjs b/.rovodev/skills/impeccable/scripts/lib/concept-catalog.mjs index 9c20711ef..949594d0d 100644 --- a/.rovodev/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.rovodev/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes || concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) { errors.push(`concept ${id} must have exactly three structural tags`); } + // The slop this world in particular is at risk of. Optional, because 541 + // entries predate it and none of them are wrong for lacking it. A world built + // from posters is at risk of shouting and one built from instruments is at + // risk of dead greys; a global detector cannot know which, and the author can. + if (concept?.avoid !== undefined) { + if (!Array.isArray(concept.avoid) + || concept.avoid.length < 2 + || concept.avoid.length > 3 + || concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) { + errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`); + } + } if (!Array.isArray(concept?.system) || concept.system.length !== SYSTEM_PREFIXES.length || concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) { 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/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/open-system-browser.mjs b/.rovodev/skills/impeccable/scripts/lib/open-system-browser.mjs new file mode 100644 index 000000000..c44cd847a --- /dev/null +++ b/.rovodev/skills/impeccable/scripts/lib/open-system-browser.mjs @@ -0,0 +1,26 @@ +import { spawn } from 'node:child_process'; + +export function browserOpenCommand(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', +} = {}) { + if (platform === 'darwin') return { command: 'open', args: [url] }; + if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] }; + return { command: 'xdg-open', args: [url] }; +} + +export function openSystemBrowser(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', + spawnImpl = spawn, +} = {}) { + const { command, args } = browserOpenCommand(url, { platform, comspec }); + try { + const child = spawnImpl(command, args, { stdio: 'ignore', detached: true }); + child.on('error', () => {}); + child.unref(); + return true; + } catch { + return false; + } +} diff --git a/.rovodev/skills/impeccable/scripts/lib/roll-selection.mjs b/.rovodev/skills/impeccable/scripts/lib/roll-selection.mjs index e3c9efbb8..6fab19396 100644 --- a/.rovodev/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.rovodev/skills/impeccable/scripts/lib/roll-selection.mjs @@ -96,31 +96,38 @@ function* rank(items, input, idFor = item => item.id) { .map(entry => entry.item); } -// Two independent exclusions, and either one is enough to hold a world back. -// Rating grades quality: a 3-star earns a second ticket, a 1-star marginal keep -// leaves the pool. Breadth says whether a world can serve an arbitrary build at -// all, so a niche world leaves however good it is, keeping its approval for -// direct briefs. Breadth was split out of rating because the only way to hold a -// narrow world back used to be calling it marginal, which made "excellent but -// narrow" unrecordable and corrupted ratings as a calibration signal. +// Rating sets how many tickets a world holds; breadth decides whether it draws +// at all. A niche world leaves the pool however good it is, keeping its approval +// for direct briefs. Breadth was split out of rating because the only way to +// hold a narrow world back used to be calling it marginal, which made "excellent +// but narrow" unrecordable and corrupted ratings as a calibration signal. +// +// Two tickets for a 3-star, one for everything else, was too sharp. Measured +// against the catalog as it stood: 3-star worlds absorbed 57% of the graphic +// draw from 65 of 163 eligible worlds, 46% of atmosphere from 13 of 43, and +// 75% of interaction from 15 of 25. The reviewer's complaint, that the same +// worlds keep coming back, is what a rating multiplier does to a pool whose +// thinnest tier holds 25 worlds. +// +// So a 3-star no longer outdraws a 2-star, and a 1-star draws at half rather +// than not at all. A marginal keep is still worth showing sometimes: the +// judgement it records is "narrow or unexceptional", not "wrong", and excluding +// it entirely made a rating do a job breadth already does properly. +const RATING_TICKETS = { 1: 1, 2: 2, 3: 2 }; +const ticketsForRating = rating => RATING_TICKETS[rating] ?? 2; + function challengerTickets(pool) { return pool.flatMap(concept => { - const rating = concept.review?.rating; - if (rating === 1 || concept.review?.breadth === 'niche') return []; - return rating === 3 - ? [{ concept, ticket: 0 }, { concept, ticket: 1 }] - : [{ concept, ticket: 0 }]; + if (concept.review?.breadth === 'niche') return []; + return Array.from({ length: ticketsForRating(concept.review?.rating) }, + (_, ticket) => ({ concept, ticket })); }); } function compositionTickets(pool) { - return pool.flatMap(composition => { - const rating = composition.review?.rating; - if (rating === 1) return []; - return rating === 3 - ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] - : [{ composition, ticket: 0 }]; - }); + return pool.flatMap(composition => Array.from( + { length: ticketsForRating(composition.review?.rating) }, + (_, ticket) => ({ composition, ticket }))); } /** 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-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.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/.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 `; } @@ -943,22 +1118,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; } @@ -976,8 +1158,7 @@ server.listen(portArg, '127.0.0.1', () => { console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...'); } if (!hasFlag('no-open')) { - const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'; - try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ } + openSystemBrowser(url); } if (timeoutSec > 0) { setTimeout(() => { diff --git a/.trae-cn/skills/impeccable/SKILL.md b/.trae-cn/skills/impeccable/SKILL.md index 660b99489..84992102d 100644 --- a/.trae-cn/skills/impeccable/SKILL.md +++ b/.trae-cn/skills/impeccable/SKILL.md @@ -12,11 +12,11 @@ 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 -1. Run `node .trae-cn/skills/impeccable/scripts/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 .trae-cn/skills/impeccable/scripts/...` command in this skill and its references, and `.trae-cn/skills/impeccable/scripts` 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/.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/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-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/craft-floor.md b/.trae-cn/skills/impeccable/reference/craft-floor.md index 408f2912e..93be921db 100644 --- a/.trae-cn/skills/impeccable/reference/craft-floor.md +++ b/.trae-cn/skills/impeccable/reference/craft-floor.md @@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe - **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows. - **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth. - **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus. +- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably. - **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery. - **Coverage:** every brief requirement present and findable within seconds. 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..e90fd9f20 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); 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 -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. @@ -34,4 +34,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. \ No newline at end of file +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. \ No newline at end of file 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 8115cd9d9..1bd18cc6c 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/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. 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,13 +80,13 @@ 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 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. ## 6. Build with full commitment -When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the comp at identical dimensions after every region, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. +When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the freshly reopened comp image at identical dimensions after every region, never beside your memory of it, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. The comp also outranks every written record of it: when the recorded brief or inventory commits to less than the comp shows, a softer texture, a sparser field, a sculpted plate reduced to flat CSS, correct the record upward to the comp; qualifiers like subtle, restrained, and low-contrast, and counts rounded down to a comfortable fraction, are how approved materials die between approval and build. A produced material must then survive to the screen: a texture buried under a nearly opaque color wash ships the wash, not the material, so judge every material by the screenshot beside the comp, never by the stylesheet. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. Build the assigned direction, not a safer interpretation of it. The form supplies structure, reading order, component conventions, and native motion; the product supplies every fact. Commit every atom: nav, buttons, inputs, and links are rebuilt in the form's vocabulary, and a stock component inside a committed form is a lapse. Land the first build fully committed; committing is the hard part, and the passes that follow exist to make the committed thing clear and effective, never to dilute it. In unattended work, the safe rendition is the known risk. @@ -101,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 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. 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-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-cn/skills/impeccable/reference/visualize.md b/.trae-cn/skills/impeccable/reference/visualize.md index 0780bbf8b..0075cf715 100644 --- a/.trae-cn/skills/impeccable/reference/visualize.md +++ b/.trae-cn/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) 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. ## 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/concept-seed.mjs b/.trae-cn/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..db638ab57 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, @@ -553,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/context-signals.mjs b/.trae-cn/skills/impeccable/scripts/context-signals.mjs index 743bb220a..e56214be1 100644 --- a/.trae-cn/skills/impeccable/scripts/context-signals.mjs +++ b/.trae-cn/skills/impeccable/scripts/context-signals.mjs @@ -22,7 +22,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { execFileSync } from 'node:child_process'; import { loadContext, extractPlatform } from './context.mjs'; -import { getCritiqueDir } from './lib/impeccable-paths.mjs'; +import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs'; /** Is there code here at all, or just context files / an empty repo? */ function hasCode(cwd) { @@ -34,23 +34,13 @@ function hasCode(cwd) { } /** - * The most recent critique snapshot across all targets. Filenames are - * timestamp-prefixed (`__.md`), so a lexical sort is chronological. - * Parses the small frontmatter for score + P0/P1 counts. + * Summarize the most recent critique snapshot across all targets. */ function latestCritique(cwd) { try { - const dir = getCritiqueDir(cwd); - if (!fs.existsSync(dir)) return null; - const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort(); - if (!files.length) return null; - const newest = files[files.length - 1]; - const text = fs.readFileSync(path.join(dir, newest), 'utf-8'); - const front = text.split('---')[1] || ''; - const get = (k) => { - const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm')); - return m ? m[1].trim() : null; - }; + const latest = readLatestSnapshotAcrossTargets({ cwd }); + if (!latest) return null; + const get = (key) => latest.meta[key] ?? null; const num = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null; @@ -61,7 +51,7 @@ function latestCritique(cwd) { p0: num(get('p0')), p1: num(get('p1')), timestamp: get('timestamp'), - file: path.relative(cwd, path.join(dir, newest)), + file: path.relative(cwd, latest.path), }; } catch { return null; diff --git a/.trae-cn/skills/impeccable/scripts/critique-storage.mjs b/.trae-cn/skills/impeccable/scripts/critique-storage.mjs index a8b36b025..f23fded37 100644 --- a/.trae-cn/skills/impeccable/scripts/critique-storage.mjs +++ b/.trae-cn/skills/impeccable/scripts/critique-storage.mjs @@ -105,28 +105,37 @@ function parseFrontmatter(text) { } /** - * Return all snapshot files for `slug`, sorted oldest → newest. + * Return snapshot files matching `suffix`, sorted oldest → newest. */ -function listSnapshotsForSlug(slug, cwd) { +const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/; + +function listSnapshots(suffix, cwd) { const dir = getCritiqueDir(cwd); if (!fs.existsSync(dir)) return []; - const suffix = `__${slug}.md`; return fs.readdirSync(dir) - .filter((f) => f.endsWith(suffix)) + .filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix)) .sort() .map((f) => path.join(dir, f)); } +function readLatestSnapshotMatching(suffix, cwd) { + const filePath = listSnapshots(suffix, cwd).at(-1); + if (!filePath) return null; + const body = fs.readFileSync(filePath, 'utf-8'); + return { path: filePath, body, meta: parseFrontmatter(body) }; +} + /** * Return the most recent snapshot for `slug`, or null. Polish reads this * to find its fix backlog when the slug matches. */ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); - if (!all.length) return null; - const latest = all[all.length - 1]; - const body = fs.readFileSync(latest, 'utf-8'); - return { path: latest, body, meta: parseFrontmatter(body) }; + return readLatestSnapshotMatching(`__${slug}.md`, cwd); +} + +/** Return the most recent snapshot across all targets, or null. */ +export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) { + return readLatestSnapshotMatching('.md', cwd); } /** @@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { * Critique appends a one-line trend to its output using this. */ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); + const all = listSnapshots(`__${slug}.md`, cwd); const slice = all.slice(-limit); return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8'))); } diff --git a/.trae-cn/skills/impeccable/scripts/detector/detect-antipatterns.mjs b/.trae-cn/skills/impeccable/scripts/detector/detect-antipatterns.mjs index c5bcf064c..e88397e37 100644 --- a/.trae-cn/skills/impeccable/scripts/detector/detect-antipatterns.mjs +++ b/.trae-cn/skills/impeccable/scripts/detector/detect-antipatterns.mjs @@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs'; export { walkDir, + hasScannableExtension, SCANNABLE_EXTENSIONS, SKIP_DIRS, buildImportGraph, diff --git a/.trae-cn/skills/impeccable/scripts/detector/node/file-system.mjs b/.trae-cn/skills/impeccable/scripts/detector/node/file-system.mjs index 6a74fa353..964f6712d 100644 --- a/.trae-cn/skills/impeccable/scripts/detector/node/file-system.mjs +++ b/.trae-cn/skills/impeccable/scripts/detector/node/file-system.mjs @@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']); const SCANNABLE_EXTENSIONS = new Set([ '.html', '.htm', '.css', '.scss', '.sass', '.less', '.jsx', '.tsx', '.js', '.ts', - '.vue', '.svelte', '.astro', + '.vue', '.svelte', '.astro', '.blade.php', ]); const HTML_EXTENSIONS = new Set(['.html', '.htm']); +function hasScannableExtension(filename) { + const lower = filename.toLowerCase(); + if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true; + for (const ext of SCANNABLE_EXTENSIONS) { + if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true; + } + return false; +} + const IMPORT_SPECIFIER_PATTERNS = [ /import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g, /@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g, @@ -46,7 +55,7 @@ function walkDir(dir) { if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue; const full = path.join(dir, entry.name); if (entry.isDirectory()) files.push(...walkDir(full)); - else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full); + else if (hasScannableExtension(entry.name)) files.push(full); } return files; } @@ -194,6 +203,7 @@ export { SKIP_DIRS, SCANNABLE_EXTENSIONS, HTML_EXTENSIONS, + hasScannableExtension, walkDir, resolveImport, buildImportGraph, 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/concept-catalog.mjs b/.trae-cn/skills/impeccable/scripts/lib/concept-catalog.mjs index 9c20711ef..949594d0d 100644 --- a/.trae-cn/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.trae-cn/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes || concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) { errors.push(`concept ${id} must have exactly three structural tags`); } + // The slop this world in particular is at risk of. Optional, because 541 + // entries predate it and none of them are wrong for lacking it. A world built + // from posters is at risk of shouting and one built from instruments is at + // risk of dead greys; a global detector cannot know which, and the author can. + if (concept?.avoid !== undefined) { + if (!Array.isArray(concept.avoid) + || concept.avoid.length < 2 + || concept.avoid.length > 3 + || concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) { + errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`); + } + } if (!Array.isArray(concept?.system) || concept.system.length !== SYSTEM_PREFIXES.length || concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) { 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/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/open-system-browser.mjs b/.trae-cn/skills/impeccable/scripts/lib/open-system-browser.mjs new file mode 100644 index 000000000..c44cd847a --- /dev/null +++ b/.trae-cn/skills/impeccable/scripts/lib/open-system-browser.mjs @@ -0,0 +1,26 @@ +import { spawn } from 'node:child_process'; + +export function browserOpenCommand(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', +} = {}) { + if (platform === 'darwin') return { command: 'open', args: [url] }; + if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] }; + return { command: 'xdg-open', args: [url] }; +} + +export function openSystemBrowser(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', + spawnImpl = spawn, +} = {}) { + const { command, args } = browserOpenCommand(url, { platform, comspec }); + try { + const child = spawnImpl(command, args, { stdio: 'ignore', detached: true }); + child.on('error', () => {}); + child.unref(); + return true; + } catch { + return false; + } +} diff --git a/.trae-cn/skills/impeccable/scripts/lib/roll-selection.mjs b/.trae-cn/skills/impeccable/scripts/lib/roll-selection.mjs index e3c9efbb8..6fab19396 100644 --- a/.trae-cn/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.trae-cn/skills/impeccable/scripts/lib/roll-selection.mjs @@ -96,31 +96,38 @@ function* rank(items, input, idFor = item => item.id) { .map(entry => entry.item); } -// Two independent exclusions, and either one is enough to hold a world back. -// Rating grades quality: a 3-star earns a second ticket, a 1-star marginal keep -// leaves the pool. Breadth says whether a world can serve an arbitrary build at -// all, so a niche world leaves however good it is, keeping its approval for -// direct briefs. Breadth was split out of rating because the only way to hold a -// narrow world back used to be calling it marginal, which made "excellent but -// narrow" unrecordable and corrupted ratings as a calibration signal. +// Rating sets how many tickets a world holds; breadth decides whether it draws +// at all. A niche world leaves the pool however good it is, keeping its approval +// for direct briefs. Breadth was split out of rating because the only way to +// hold a narrow world back used to be calling it marginal, which made "excellent +// but narrow" unrecordable and corrupted ratings as a calibration signal. +// +// Two tickets for a 3-star, one for everything else, was too sharp. Measured +// against the catalog as it stood: 3-star worlds absorbed 57% of the graphic +// draw from 65 of 163 eligible worlds, 46% of atmosphere from 13 of 43, and +// 75% of interaction from 15 of 25. The reviewer's complaint, that the same +// worlds keep coming back, is what a rating multiplier does to a pool whose +// thinnest tier holds 25 worlds. +// +// So a 3-star no longer outdraws a 2-star, and a 1-star draws at half rather +// than not at all. A marginal keep is still worth showing sometimes: the +// judgement it records is "narrow or unexceptional", not "wrong", and excluding +// it entirely made a rating do a job breadth already does properly. +const RATING_TICKETS = { 1: 1, 2: 2, 3: 2 }; +const ticketsForRating = rating => RATING_TICKETS[rating] ?? 2; + function challengerTickets(pool) { return pool.flatMap(concept => { - const rating = concept.review?.rating; - if (rating === 1 || concept.review?.breadth === 'niche') return []; - return rating === 3 - ? [{ concept, ticket: 0 }, { concept, ticket: 1 }] - : [{ concept, ticket: 0 }]; + if (concept.review?.breadth === 'niche') return []; + return Array.from({ length: ticketsForRating(concept.review?.rating) }, + (_, ticket) => ({ concept, ticket })); }); } function compositionTickets(pool) { - return pool.flatMap(composition => { - const rating = composition.review?.rating; - if (rating === 1) return []; - return rating === 3 - ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] - : [{ composition, ticket: 0 }]; - }); + return pool.flatMap(composition => Array.from( + { length: ticketsForRating(composition.review?.rating) }, + (_, ticket) => ({ composition, ticket }))); } /** 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-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.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-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 `; } @@ -943,22 +1118,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; } @@ -976,8 +1158,7 @@ server.listen(portArg, '127.0.0.1', () => { console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...'); } if (!hasFlag('no-open')) { - const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'; - try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ } + openSystemBrowser(url); } if (timeoutSec > 0) { setTimeout(() => { diff --git a/.trae/skills/impeccable/SKILL.md b/.trae/skills/impeccable/SKILL.md index d0bf93876..805d762c8 100644 --- a/.trae/skills/impeccable/SKILL.md +++ b/.trae/skills/impeccable/SKILL.md @@ -12,11 +12,11 @@ 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 -1. Run `node .trae/skills/impeccable/scripts/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 .trae/skills/impeccable/scripts/...` command in this skill and its references, and `.trae/skills/impeccable/scripts` 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/.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/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/.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/craft-floor.md b/.trae/skills/impeccable/reference/craft-floor.md index 408f2912e..93be921db 100644 --- a/.trae/skills/impeccable/reference/craft-floor.md +++ b/.trae/skills/impeccable/reference/craft-floor.md @@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe - **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows. - **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth. - **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus. +- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably. - **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery. - **Coverage:** every brief requirement present and findable within seconds. 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..e90fd9f20 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); 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 -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. @@ -34,4 +34,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. \ No newline at end of file +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. \ No newline at end of file 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 1d8c11bac..aa6defef0 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/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. 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,13 +80,13 @@ 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 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. ## 6. Build with full commitment -When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the comp at identical dimensions after every region, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. +When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the freshly reopened comp image at identical dimensions after every region, never beside your memory of it, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. The comp also outranks every written record of it: when the recorded brief or inventory commits to less than the comp shows, a softer texture, a sparser field, a sculpted plate reduced to flat CSS, correct the record upward to the comp; qualifiers like subtle, restrained, and low-contrast, and counts rounded down to a comfortable fraction, are how approved materials die between approval and build. A produced material must then survive to the screen: a texture buried under a nearly opaque color wash ships the wash, not the material, so judge every material by the screenshot beside the comp, never by the stylesheet. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. Build the assigned direction, not a safer interpretation of it. The form supplies structure, reading order, component conventions, and native motion; the product supplies every fact. Commit every atom: nav, buttons, inputs, and links are rebuilt in the form's vocabulary, and a stock component inside a committed form is a lapse. Land the first build fully committed; committing is the hard part, and the passes that follow exist to make the committed thing clear and effective, never to dilute it. In unattended work, the safe rendition is the known risk. @@ -101,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 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. 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/.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/.trae/skills/impeccable/reference/visualize.md b/.trae/skills/impeccable/reference/visualize.md index ef675864c..096cc2e1d 100644 --- a/.trae/skills/impeccable/reference/visualize.md +++ b/.trae/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) 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. ## 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/concept-seed.mjs b/.trae/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..db638ab57 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, @@ -553,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/context-signals.mjs b/.trae/skills/impeccable/scripts/context-signals.mjs index 743bb220a..e56214be1 100644 --- a/.trae/skills/impeccable/scripts/context-signals.mjs +++ b/.trae/skills/impeccable/scripts/context-signals.mjs @@ -22,7 +22,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { execFileSync } from 'node:child_process'; import { loadContext, extractPlatform } from './context.mjs'; -import { getCritiqueDir } from './lib/impeccable-paths.mjs'; +import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs'; /** Is there code here at all, or just context files / an empty repo? */ function hasCode(cwd) { @@ -34,23 +34,13 @@ function hasCode(cwd) { } /** - * The most recent critique snapshot across all targets. Filenames are - * timestamp-prefixed (`__.md`), so a lexical sort is chronological. - * Parses the small frontmatter for score + P0/P1 counts. + * Summarize the most recent critique snapshot across all targets. */ function latestCritique(cwd) { try { - const dir = getCritiqueDir(cwd); - if (!fs.existsSync(dir)) return null; - const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort(); - if (!files.length) return null; - const newest = files[files.length - 1]; - const text = fs.readFileSync(path.join(dir, newest), 'utf-8'); - const front = text.split('---')[1] || ''; - const get = (k) => { - const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm')); - return m ? m[1].trim() : null; - }; + const latest = readLatestSnapshotAcrossTargets({ cwd }); + if (!latest) return null; + const get = (key) => latest.meta[key] ?? null; const num = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null; @@ -61,7 +51,7 @@ function latestCritique(cwd) { p0: num(get('p0')), p1: num(get('p1')), timestamp: get('timestamp'), - file: path.relative(cwd, path.join(dir, newest)), + file: path.relative(cwd, latest.path), }; } catch { return null; diff --git a/.trae/skills/impeccable/scripts/critique-storage.mjs b/.trae/skills/impeccable/scripts/critique-storage.mjs index a8b36b025..f23fded37 100644 --- a/.trae/skills/impeccable/scripts/critique-storage.mjs +++ b/.trae/skills/impeccable/scripts/critique-storage.mjs @@ -105,28 +105,37 @@ function parseFrontmatter(text) { } /** - * Return all snapshot files for `slug`, sorted oldest → newest. + * Return snapshot files matching `suffix`, sorted oldest → newest. */ -function listSnapshotsForSlug(slug, cwd) { +const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/; + +function listSnapshots(suffix, cwd) { const dir = getCritiqueDir(cwd); if (!fs.existsSync(dir)) return []; - const suffix = `__${slug}.md`; return fs.readdirSync(dir) - .filter((f) => f.endsWith(suffix)) + .filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix)) .sort() .map((f) => path.join(dir, f)); } +function readLatestSnapshotMatching(suffix, cwd) { + const filePath = listSnapshots(suffix, cwd).at(-1); + if (!filePath) return null; + const body = fs.readFileSync(filePath, 'utf-8'); + return { path: filePath, body, meta: parseFrontmatter(body) }; +} + /** * Return the most recent snapshot for `slug`, or null. Polish reads this * to find its fix backlog when the slug matches. */ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); - if (!all.length) return null; - const latest = all[all.length - 1]; - const body = fs.readFileSync(latest, 'utf-8'); - return { path: latest, body, meta: parseFrontmatter(body) }; + return readLatestSnapshotMatching(`__${slug}.md`, cwd); +} + +/** Return the most recent snapshot across all targets, or null. */ +export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) { + return readLatestSnapshotMatching('.md', cwd); } /** @@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { * Critique appends a one-line trend to its output using this. */ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); + const all = listSnapshots(`__${slug}.md`, cwd); const slice = all.slice(-limit); return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8'))); } diff --git a/.trae/skills/impeccable/scripts/detector/detect-antipatterns.mjs b/.trae/skills/impeccable/scripts/detector/detect-antipatterns.mjs index c5bcf064c..e88397e37 100644 --- a/.trae/skills/impeccable/scripts/detector/detect-antipatterns.mjs +++ b/.trae/skills/impeccable/scripts/detector/detect-antipatterns.mjs @@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs'; export { walkDir, + hasScannableExtension, SCANNABLE_EXTENSIONS, SKIP_DIRS, buildImportGraph, diff --git a/.trae/skills/impeccable/scripts/detector/node/file-system.mjs b/.trae/skills/impeccable/scripts/detector/node/file-system.mjs index 6a74fa353..964f6712d 100644 --- a/.trae/skills/impeccable/scripts/detector/node/file-system.mjs +++ b/.trae/skills/impeccable/scripts/detector/node/file-system.mjs @@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']); const SCANNABLE_EXTENSIONS = new Set([ '.html', '.htm', '.css', '.scss', '.sass', '.less', '.jsx', '.tsx', '.js', '.ts', - '.vue', '.svelte', '.astro', + '.vue', '.svelte', '.astro', '.blade.php', ]); const HTML_EXTENSIONS = new Set(['.html', '.htm']); +function hasScannableExtension(filename) { + const lower = filename.toLowerCase(); + if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true; + for (const ext of SCANNABLE_EXTENSIONS) { + if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true; + } + return false; +} + const IMPORT_SPECIFIER_PATTERNS = [ /import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g, /@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g, @@ -46,7 +55,7 @@ function walkDir(dir) { if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue; const full = path.join(dir, entry.name); if (entry.isDirectory()) files.push(...walkDir(full)); - else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full); + else if (hasScannableExtension(entry.name)) files.push(full); } return files; } @@ -194,6 +203,7 @@ export { SKIP_DIRS, SCANNABLE_EXTENSIONS, HTML_EXTENSIONS, + hasScannableExtension, walkDir, resolveImport, buildImportGraph, 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/concept-catalog.mjs b/.trae/skills/impeccable/scripts/lib/concept-catalog.mjs index 9c20711ef..949594d0d 100644 --- a/.trae/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.trae/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes || concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) { errors.push(`concept ${id} must have exactly three structural tags`); } + // The slop this world in particular is at risk of. Optional, because 541 + // entries predate it and none of them are wrong for lacking it. A world built + // from posters is at risk of shouting and one built from instruments is at + // risk of dead greys; a global detector cannot know which, and the author can. + if (concept?.avoid !== undefined) { + if (!Array.isArray(concept.avoid) + || concept.avoid.length < 2 + || concept.avoid.length > 3 + || concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) { + errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`); + } + } if (!Array.isArray(concept?.system) || concept.system.length !== SYSTEM_PREFIXES.length || concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) { 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/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/open-system-browser.mjs b/.trae/skills/impeccable/scripts/lib/open-system-browser.mjs new file mode 100644 index 000000000..c44cd847a --- /dev/null +++ b/.trae/skills/impeccable/scripts/lib/open-system-browser.mjs @@ -0,0 +1,26 @@ +import { spawn } from 'node:child_process'; + +export function browserOpenCommand(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', +} = {}) { + if (platform === 'darwin') return { command: 'open', args: [url] }; + if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] }; + return { command: 'xdg-open', args: [url] }; +} + +export function openSystemBrowser(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', + spawnImpl = spawn, +} = {}) { + const { command, args } = browserOpenCommand(url, { platform, comspec }); + try { + const child = spawnImpl(command, args, { stdio: 'ignore', detached: true }); + child.on('error', () => {}); + child.unref(); + return true; + } catch { + return false; + } +} diff --git a/.trae/skills/impeccable/scripts/lib/roll-selection.mjs b/.trae/skills/impeccable/scripts/lib/roll-selection.mjs index e3c9efbb8..6fab19396 100644 --- a/.trae/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.trae/skills/impeccable/scripts/lib/roll-selection.mjs @@ -96,31 +96,38 @@ function* rank(items, input, idFor = item => item.id) { .map(entry => entry.item); } -// Two independent exclusions, and either one is enough to hold a world back. -// Rating grades quality: a 3-star earns a second ticket, a 1-star marginal keep -// leaves the pool. Breadth says whether a world can serve an arbitrary build at -// all, so a niche world leaves however good it is, keeping its approval for -// direct briefs. Breadth was split out of rating because the only way to hold a -// narrow world back used to be calling it marginal, which made "excellent but -// narrow" unrecordable and corrupted ratings as a calibration signal. +// Rating sets how many tickets a world holds; breadth decides whether it draws +// at all. A niche world leaves the pool however good it is, keeping its approval +// for direct briefs. Breadth was split out of rating because the only way to +// hold a narrow world back used to be calling it marginal, which made "excellent +// but narrow" unrecordable and corrupted ratings as a calibration signal. +// +// Two tickets for a 3-star, one for everything else, was too sharp. Measured +// against the catalog as it stood: 3-star worlds absorbed 57% of the graphic +// draw from 65 of 163 eligible worlds, 46% of atmosphere from 13 of 43, and +// 75% of interaction from 15 of 25. The reviewer's complaint, that the same +// worlds keep coming back, is what a rating multiplier does to a pool whose +// thinnest tier holds 25 worlds. +// +// So a 3-star no longer outdraws a 2-star, and a 1-star draws at half rather +// than not at all. A marginal keep is still worth showing sometimes: the +// judgement it records is "narrow or unexceptional", not "wrong", and excluding +// it entirely made a rating do a job breadth already does properly. +const RATING_TICKETS = { 1: 1, 2: 2, 3: 2 }; +const ticketsForRating = rating => RATING_TICKETS[rating] ?? 2; + function challengerTickets(pool) { return pool.flatMap(concept => { - const rating = concept.review?.rating; - if (rating === 1 || concept.review?.breadth === 'niche') return []; - return rating === 3 - ? [{ concept, ticket: 0 }, { concept, ticket: 1 }] - : [{ concept, ticket: 0 }]; + if (concept.review?.breadth === 'niche') return []; + return Array.from({ length: ticketsForRating(concept.review?.rating) }, + (_, ticket) => ({ concept, ticket })); }); } function compositionTickets(pool) { - return pool.flatMap(composition => { - const rating = composition.review?.rating; - if (rating === 1) return []; - return rating === 3 - ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] - : [{ composition, ticket: 0 }]; - }); + return pool.flatMap(composition => Array.from( + { length: ticketsForRating(composition.review?.rating) }, + (_, ticket) => ({ composition, ticket }))); } /** 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-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.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/.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 `; } @@ -943,22 +1118,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; } @@ -976,8 +1158,7 @@ server.listen(portArg, '127.0.0.1', () => { console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...'); } if (!hasFlag('no-open')) { - const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'; - try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ } + openSystemBrowser(url); } if (timeoutSec > 0) { setTimeout(() => { diff --git a/.vibe/skills/impeccable/SKILL.md b/.vibe/skills/impeccable/SKILL.md index ef911bf1d..780ccdec0 100644 --- a/.vibe/skills/impeccable/SKILL.md +++ b/.vibe/skills/impeccable/SKILL.md @@ -14,11 +14,11 @@ 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 -1. Run `node .vibe/skills/impeccable/scripts/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 .vibe/skills/impeccable/scripts/...` command in this skill and its references, and `.vibe/skills/impeccable/scripts` 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/.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/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/.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/craft-floor.md b/.vibe/skills/impeccable/reference/craft-floor.md index 408f2912e..93be921db 100644 --- a/.vibe/skills/impeccable/reference/craft-floor.md +++ b/.vibe/skills/impeccable/reference/craft-floor.md @@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe - **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows. - **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth. - **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus. +- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably. - **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery. - **Coverage:** every brief requirement present and findable within seconds. 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..e90fd9f20 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); 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 -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. @@ -34,4 +34,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. \ No newline at end of file +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. \ No newline at end of file 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 620301c57..6d625bd64 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/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. 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,13 +80,13 @@ 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 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. ## 6. Build with full commitment -When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the comp at identical dimensions after every region, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. +When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the freshly reopened comp image at identical dimensions after every region, never beside your memory of it, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. The comp also outranks every written record of it: when the recorded brief or inventory commits to less than the comp shows, a softer texture, a sparser field, a sculpted plate reduced to flat CSS, correct the record upward to the comp; qualifiers like subtle, restrained, and low-contrast, and counts rounded down to a comfortable fraction, are how approved materials die between approval and build. A produced material must then survive to the screen: a texture buried under a nearly opaque color wash ships the wash, not the material, so judge every material by the screenshot beside the comp, never by the stylesheet. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. Build the assigned direction, not a safer interpretation of it. The form supplies structure, reading order, component conventions, and native motion; the product supplies every fact. Commit every atom: nav, buttons, inputs, and links are rebuilt in the form's vocabulary, and a stock component inside a committed form is a lapse. Land the first build fully committed; committing is the hard part, and the passes that follow exist to make the committed thing clear and effective, never to dilute it. In unattended work, the safe rendition is the known risk. @@ -101,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 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. 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/.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/.vibe/skills/impeccable/reference/visualize.md b/.vibe/skills/impeccable/reference/visualize.md index 87e410295..172762e9c 100644 --- a/.vibe/skills/impeccable/reference/visualize.md +++ b/.vibe/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) 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. ## 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/concept-seed.mjs b/.vibe/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..db638ab57 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, @@ -553,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/context-signals.mjs b/.vibe/skills/impeccable/scripts/context-signals.mjs index 743bb220a..e56214be1 100644 --- a/.vibe/skills/impeccable/scripts/context-signals.mjs +++ b/.vibe/skills/impeccable/scripts/context-signals.mjs @@ -22,7 +22,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { execFileSync } from 'node:child_process'; import { loadContext, extractPlatform } from './context.mjs'; -import { getCritiqueDir } from './lib/impeccable-paths.mjs'; +import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs'; /** Is there code here at all, or just context files / an empty repo? */ function hasCode(cwd) { @@ -34,23 +34,13 @@ function hasCode(cwd) { } /** - * The most recent critique snapshot across all targets. Filenames are - * timestamp-prefixed (`__.md`), so a lexical sort is chronological. - * Parses the small frontmatter for score + P0/P1 counts. + * Summarize the most recent critique snapshot across all targets. */ function latestCritique(cwd) { try { - const dir = getCritiqueDir(cwd); - if (!fs.existsSync(dir)) return null; - const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort(); - if (!files.length) return null; - const newest = files[files.length - 1]; - const text = fs.readFileSync(path.join(dir, newest), 'utf-8'); - const front = text.split('---')[1] || ''; - const get = (k) => { - const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm')); - return m ? m[1].trim() : null; - }; + const latest = readLatestSnapshotAcrossTargets({ cwd }); + if (!latest) return null; + const get = (key) => latest.meta[key] ?? null; const num = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null; @@ -61,7 +51,7 @@ function latestCritique(cwd) { p0: num(get('p0')), p1: num(get('p1')), timestamp: get('timestamp'), - file: path.relative(cwd, path.join(dir, newest)), + file: path.relative(cwd, latest.path), }; } catch { return null; diff --git a/.vibe/skills/impeccable/scripts/critique-storage.mjs b/.vibe/skills/impeccable/scripts/critique-storage.mjs index a8b36b025..f23fded37 100644 --- a/.vibe/skills/impeccable/scripts/critique-storage.mjs +++ b/.vibe/skills/impeccable/scripts/critique-storage.mjs @@ -105,28 +105,37 @@ function parseFrontmatter(text) { } /** - * Return all snapshot files for `slug`, sorted oldest → newest. + * Return snapshot files matching `suffix`, sorted oldest → newest. */ -function listSnapshotsForSlug(slug, cwd) { +const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/; + +function listSnapshots(suffix, cwd) { const dir = getCritiqueDir(cwd); if (!fs.existsSync(dir)) return []; - const suffix = `__${slug}.md`; return fs.readdirSync(dir) - .filter((f) => f.endsWith(suffix)) + .filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix)) .sort() .map((f) => path.join(dir, f)); } +function readLatestSnapshotMatching(suffix, cwd) { + const filePath = listSnapshots(suffix, cwd).at(-1); + if (!filePath) return null; + const body = fs.readFileSync(filePath, 'utf-8'); + return { path: filePath, body, meta: parseFrontmatter(body) }; +} + /** * Return the most recent snapshot for `slug`, or null. Polish reads this * to find its fix backlog when the slug matches. */ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); - if (!all.length) return null; - const latest = all[all.length - 1]; - const body = fs.readFileSync(latest, 'utf-8'); - return { path: latest, body, meta: parseFrontmatter(body) }; + return readLatestSnapshotMatching(`__${slug}.md`, cwd); +} + +/** Return the most recent snapshot across all targets, or null. */ +export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) { + return readLatestSnapshotMatching('.md', cwd); } /** @@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { * Critique appends a one-line trend to its output using this. */ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); + const all = listSnapshots(`__${slug}.md`, cwd); const slice = all.slice(-limit); return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8'))); } diff --git a/.vibe/skills/impeccable/scripts/detector/detect-antipatterns.mjs b/.vibe/skills/impeccable/scripts/detector/detect-antipatterns.mjs index c5bcf064c..e88397e37 100644 --- a/.vibe/skills/impeccable/scripts/detector/detect-antipatterns.mjs +++ b/.vibe/skills/impeccable/scripts/detector/detect-antipatterns.mjs @@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs'; export { walkDir, + hasScannableExtension, SCANNABLE_EXTENSIONS, SKIP_DIRS, buildImportGraph, diff --git a/.vibe/skills/impeccable/scripts/detector/node/file-system.mjs b/.vibe/skills/impeccable/scripts/detector/node/file-system.mjs index 6a74fa353..964f6712d 100644 --- a/.vibe/skills/impeccable/scripts/detector/node/file-system.mjs +++ b/.vibe/skills/impeccable/scripts/detector/node/file-system.mjs @@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']); const SCANNABLE_EXTENSIONS = new Set([ '.html', '.htm', '.css', '.scss', '.sass', '.less', '.jsx', '.tsx', '.js', '.ts', - '.vue', '.svelte', '.astro', + '.vue', '.svelte', '.astro', '.blade.php', ]); const HTML_EXTENSIONS = new Set(['.html', '.htm']); +function hasScannableExtension(filename) { + const lower = filename.toLowerCase(); + if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true; + for (const ext of SCANNABLE_EXTENSIONS) { + if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true; + } + return false; +} + const IMPORT_SPECIFIER_PATTERNS = [ /import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g, /@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g, @@ -46,7 +55,7 @@ function walkDir(dir) { if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue; const full = path.join(dir, entry.name); if (entry.isDirectory()) files.push(...walkDir(full)); - else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full); + else if (hasScannableExtension(entry.name)) files.push(full); } return files; } @@ -194,6 +203,7 @@ export { SKIP_DIRS, SCANNABLE_EXTENSIONS, HTML_EXTENSIONS, + hasScannableExtension, walkDir, resolveImport, buildImportGraph, 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/concept-catalog.mjs b/.vibe/skills/impeccable/scripts/lib/concept-catalog.mjs index 9c20711ef..949594d0d 100644 --- a/.vibe/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.vibe/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes || concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) { errors.push(`concept ${id} must have exactly three structural tags`); } + // The slop this world in particular is at risk of. Optional, because 541 + // entries predate it and none of them are wrong for lacking it. A world built + // from posters is at risk of shouting and one built from instruments is at + // risk of dead greys; a global detector cannot know which, and the author can. + if (concept?.avoid !== undefined) { + if (!Array.isArray(concept.avoid) + || concept.avoid.length < 2 + || concept.avoid.length > 3 + || concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) { + errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`); + } + } if (!Array.isArray(concept?.system) || concept.system.length !== SYSTEM_PREFIXES.length || concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) { 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/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/open-system-browser.mjs b/.vibe/skills/impeccable/scripts/lib/open-system-browser.mjs new file mode 100644 index 000000000..c44cd847a --- /dev/null +++ b/.vibe/skills/impeccable/scripts/lib/open-system-browser.mjs @@ -0,0 +1,26 @@ +import { spawn } from 'node:child_process'; + +export function browserOpenCommand(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', +} = {}) { + if (platform === 'darwin') return { command: 'open', args: [url] }; + if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] }; + return { command: 'xdg-open', args: [url] }; +} + +export function openSystemBrowser(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', + spawnImpl = spawn, +} = {}) { + const { command, args } = browserOpenCommand(url, { platform, comspec }); + try { + const child = spawnImpl(command, args, { stdio: 'ignore', detached: true }); + child.on('error', () => {}); + child.unref(); + return true; + } catch { + return false; + } +} diff --git a/.vibe/skills/impeccable/scripts/lib/roll-selection.mjs b/.vibe/skills/impeccable/scripts/lib/roll-selection.mjs index e3c9efbb8..6fab19396 100644 --- a/.vibe/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.vibe/skills/impeccable/scripts/lib/roll-selection.mjs @@ -96,31 +96,38 @@ function* rank(items, input, idFor = item => item.id) { .map(entry => entry.item); } -// Two independent exclusions, and either one is enough to hold a world back. -// Rating grades quality: a 3-star earns a second ticket, a 1-star marginal keep -// leaves the pool. Breadth says whether a world can serve an arbitrary build at -// all, so a niche world leaves however good it is, keeping its approval for -// direct briefs. Breadth was split out of rating because the only way to hold a -// narrow world back used to be calling it marginal, which made "excellent but -// narrow" unrecordable and corrupted ratings as a calibration signal. +// Rating sets how many tickets a world holds; breadth decides whether it draws +// at all. A niche world leaves the pool however good it is, keeping its approval +// for direct briefs. Breadth was split out of rating because the only way to +// hold a narrow world back used to be calling it marginal, which made "excellent +// but narrow" unrecordable and corrupted ratings as a calibration signal. +// +// Two tickets for a 3-star, one for everything else, was too sharp. Measured +// against the catalog as it stood: 3-star worlds absorbed 57% of the graphic +// draw from 65 of 163 eligible worlds, 46% of atmosphere from 13 of 43, and +// 75% of interaction from 15 of 25. The reviewer's complaint, that the same +// worlds keep coming back, is what a rating multiplier does to a pool whose +// thinnest tier holds 25 worlds. +// +// So a 3-star no longer outdraws a 2-star, and a 1-star draws at half rather +// than not at all. A marginal keep is still worth showing sometimes: the +// judgement it records is "narrow or unexceptional", not "wrong", and excluding +// it entirely made a rating do a job breadth already does properly. +const RATING_TICKETS = { 1: 1, 2: 2, 3: 2 }; +const ticketsForRating = rating => RATING_TICKETS[rating] ?? 2; + function challengerTickets(pool) { return pool.flatMap(concept => { - const rating = concept.review?.rating; - if (rating === 1 || concept.review?.breadth === 'niche') return []; - return rating === 3 - ? [{ concept, ticket: 0 }, { concept, ticket: 1 }] - : [{ concept, ticket: 0 }]; + if (concept.review?.breadth === 'niche') return []; + return Array.from({ length: ticketsForRating(concept.review?.rating) }, + (_, ticket) => ({ concept, ticket })); }); } function compositionTickets(pool) { - return pool.flatMap(composition => { - const rating = composition.review?.rating; - if (rating === 1) return []; - return rating === 3 - ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] - : [{ composition, ticket: 0 }]; - }); + return pool.flatMap(composition => Array.from( + { length: ticketsForRating(composition.review?.rating) }, + (_, ticket) => ({ composition, ticket }))); } /** 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-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.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/.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 `; } @@ -943,22 +1118,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; } @@ -976,8 +1158,7 @@ server.listen(portArg, '127.0.0.1', () => { console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...'); } if (!hasFlag('no-open')) { - const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'; - try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ } + openSystemBrowser(url); } if (timeoutSec > 0) { setTimeout(() => { 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 5366e6375..c57c4d0a6 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 }; } /** @@ -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; } @@ -1812,16 +1828,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 +1859,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 +1877,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 +1896,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'); @@ -1926,7 +1960,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/cli/engine/detect-antipatterns.mjs b/cli/engine/detect-antipatterns.mjs index c5bcf064c..e88397e37 100644 --- a/cli/engine/detect-antipatterns.mjs +++ b/cli/engine/detect-antipatterns.mjs @@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs'; export { walkDir, + hasScannableExtension, SCANNABLE_EXTENSIONS, SKIP_DIRS, buildImportGraph, 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, { diff --git a/cli/engine/node/file-system.mjs b/cli/engine/node/file-system.mjs index 6a74fa353..964f6712d 100644 --- a/cli/engine/node/file-system.mjs +++ b/cli/engine/node/file-system.mjs @@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']); const SCANNABLE_EXTENSIONS = new Set([ '.html', '.htm', '.css', '.scss', '.sass', '.less', '.jsx', '.tsx', '.js', '.ts', - '.vue', '.svelte', '.astro', + '.vue', '.svelte', '.astro', '.blade.php', ]); const HTML_EXTENSIONS = new Set(['.html', '.htm']); +function hasScannableExtension(filename) { + const lower = filename.toLowerCase(); + if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true; + for (const ext of SCANNABLE_EXTENSIONS) { + if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true; + } + return false; +} + const IMPORT_SPECIFIER_PATTERNS = [ /import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g, /@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g, @@ -46,7 +55,7 @@ function walkDir(dir) { if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue; const full = path.join(dir, entry.name); if (entry.isDirectory()) files.push(...walkDir(full)); - else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full); + else if (hasScannableExtension(entry.name)) files.push(full); } return files; } @@ -194,6 +203,7 @@ export { SKIP_DIRS, SCANNABLE_EXTENSIONS, HTML_EXTENSIONS, + hasScannableExtension, walkDir, resolveImport, buildImportGraph, diff --git a/cli/lib/impeccable-config.mjs b/cli/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/cli/lib/impeccable-config.mjs +++ b/cli/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/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..d03529403 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); 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 -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. @@ -39,4 +39,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/plugin/skills/impeccable/SKILL.md b/plugin/skills/impeccable/SKILL.md index f89c3f92a..43920ae2b 100644 --- a/plugin/skills/impeccable/SKILL.md +++ b/plugin/skills/impeccable/SKILL.md @@ -15,11 +15,11 @@ 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 -1. Run `node .claude/skills/impeccable/scripts/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 .claude/skills/impeccable/scripts/...` command in this skill and its references, and `.claude/skills/impeccable/scripts` 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/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/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. 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/craft-floor.md b/plugin/skills/impeccable/reference/craft-floor.md index 408f2912e..93be921db 100644 --- a/plugin/skills/impeccable/reference/craft-floor.md +++ b/plugin/skills/impeccable/reference/craft-floor.md @@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe - **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows. - **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth. - **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus. +- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably. - **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery. - **Coverage:** every brief requirement present and findable within seconds. 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..e90fd9f20 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); 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 -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. @@ -34,4 +34,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. \ No newline at end of file +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. \ No newline at end of file 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 0ea9a6d21..859b84a6b 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/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. 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,13 +82,13 @@ 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 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. ## 6. Build with full commitment -When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the comp at identical dimensions after every region, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. +When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the freshly reopened comp image at identical dimensions after every region, never beside your memory of it, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. The comp also outranks every written record of it: when the recorded brief or inventory commits to less than the comp shows, a softer texture, a sparser field, a sculpted plate reduced to flat CSS, correct the record upward to the comp; qualifiers like subtle, restrained, and low-contrast, and counts rounded down to a comfortable fraction, are how approved materials die between approval and build. A produced material must then survive to the screen: a texture buried under a nearly opaque color wash ships the wash, not the material, so judge every material by the screenshot beside the comp, never by the stylesheet. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. Build the assigned direction, not a safer interpretation of it. The form supplies structure, reading order, component conventions, and native motion; the product supplies every fact. Commit every atom: nav, buttons, inputs, and links are rebuilt in the form's vocabulary, and a stock component inside a committed form is a lapse. Land the first build fully committed; committing is the hard part, and the passes that follow exist to make the committed thing clear and effective, never to dilute it. In unattended work, the safe rendition is the known risk. @@ -103,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 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. 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/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. diff --git a/plugin/skills/impeccable/reference/visualize.md b/plugin/skills/impeccable/reference/visualize.md index 94c337f15..4d91330e4 100644 --- a/plugin/skills/impeccable/reference/visualize.md +++ b/plugin/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) 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. ## 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/concept-seed.mjs b/plugin/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..db638ab57 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, @@ -553,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/context-signals.mjs b/plugin/skills/impeccable/scripts/context-signals.mjs index 743bb220a..e56214be1 100644 --- a/plugin/skills/impeccable/scripts/context-signals.mjs +++ b/plugin/skills/impeccable/scripts/context-signals.mjs @@ -22,7 +22,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { execFileSync } from 'node:child_process'; import { loadContext, extractPlatform } from './context.mjs'; -import { getCritiqueDir } from './lib/impeccable-paths.mjs'; +import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs'; /** Is there code here at all, or just context files / an empty repo? */ function hasCode(cwd) { @@ -34,23 +34,13 @@ function hasCode(cwd) { } /** - * The most recent critique snapshot across all targets. Filenames are - * timestamp-prefixed (`__.md`), so a lexical sort is chronological. - * Parses the small frontmatter for score + P0/P1 counts. + * Summarize the most recent critique snapshot across all targets. */ function latestCritique(cwd) { try { - const dir = getCritiqueDir(cwd); - if (!fs.existsSync(dir)) return null; - const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort(); - if (!files.length) return null; - const newest = files[files.length - 1]; - const text = fs.readFileSync(path.join(dir, newest), 'utf-8'); - const front = text.split('---')[1] || ''; - const get = (k) => { - const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm')); - return m ? m[1].trim() : null; - }; + const latest = readLatestSnapshotAcrossTargets({ cwd }); + if (!latest) return null; + const get = (key) => latest.meta[key] ?? null; const num = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null; @@ -61,7 +51,7 @@ function latestCritique(cwd) { p0: num(get('p0')), p1: num(get('p1')), timestamp: get('timestamp'), - file: path.relative(cwd, path.join(dir, newest)), + file: path.relative(cwd, latest.path), }; } catch { return null; diff --git a/plugin/skills/impeccable/scripts/critique-storage.mjs b/plugin/skills/impeccable/scripts/critique-storage.mjs index a8b36b025..f23fded37 100644 --- a/plugin/skills/impeccable/scripts/critique-storage.mjs +++ b/plugin/skills/impeccable/scripts/critique-storage.mjs @@ -105,28 +105,37 @@ function parseFrontmatter(text) { } /** - * Return all snapshot files for `slug`, sorted oldest → newest. + * Return snapshot files matching `suffix`, sorted oldest → newest. */ -function listSnapshotsForSlug(slug, cwd) { +const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/; + +function listSnapshots(suffix, cwd) { const dir = getCritiqueDir(cwd); if (!fs.existsSync(dir)) return []; - const suffix = `__${slug}.md`; return fs.readdirSync(dir) - .filter((f) => f.endsWith(suffix)) + .filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix)) .sort() .map((f) => path.join(dir, f)); } +function readLatestSnapshotMatching(suffix, cwd) { + const filePath = listSnapshots(suffix, cwd).at(-1); + if (!filePath) return null; + const body = fs.readFileSync(filePath, 'utf-8'); + return { path: filePath, body, meta: parseFrontmatter(body) }; +} + /** * Return the most recent snapshot for `slug`, or null. Polish reads this * to find its fix backlog when the slug matches. */ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); - if (!all.length) return null; - const latest = all[all.length - 1]; - const body = fs.readFileSync(latest, 'utf-8'); - return { path: latest, body, meta: parseFrontmatter(body) }; + return readLatestSnapshotMatching(`__${slug}.md`, cwd); +} + +/** Return the most recent snapshot across all targets, or null. */ +export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) { + return readLatestSnapshotMatching('.md', cwd); } /** @@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { * Critique appends a one-line trend to its output using this. */ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); + const all = listSnapshots(`__${slug}.md`, cwd); const slice = all.slice(-limit); return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8'))); } diff --git a/plugin/skills/impeccable/scripts/detector/detect-antipatterns.mjs b/plugin/skills/impeccable/scripts/detector/detect-antipatterns.mjs index c5bcf064c..e88397e37 100644 --- a/plugin/skills/impeccable/scripts/detector/detect-antipatterns.mjs +++ b/plugin/skills/impeccable/scripts/detector/detect-antipatterns.mjs @@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs'; export { walkDir, + hasScannableExtension, SCANNABLE_EXTENSIONS, SKIP_DIRS, buildImportGraph, diff --git a/plugin/skills/impeccable/scripts/detector/node/file-system.mjs b/plugin/skills/impeccable/scripts/detector/node/file-system.mjs index 6a74fa353..964f6712d 100644 --- a/plugin/skills/impeccable/scripts/detector/node/file-system.mjs +++ b/plugin/skills/impeccable/scripts/detector/node/file-system.mjs @@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']); const SCANNABLE_EXTENSIONS = new Set([ '.html', '.htm', '.css', '.scss', '.sass', '.less', '.jsx', '.tsx', '.js', '.ts', - '.vue', '.svelte', '.astro', + '.vue', '.svelte', '.astro', '.blade.php', ]); const HTML_EXTENSIONS = new Set(['.html', '.htm']); +function hasScannableExtension(filename) { + const lower = filename.toLowerCase(); + if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true; + for (const ext of SCANNABLE_EXTENSIONS) { + if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true; + } + return false; +} + const IMPORT_SPECIFIER_PATTERNS = [ /import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g, /@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g, @@ -46,7 +55,7 @@ function walkDir(dir) { if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue; const full = path.join(dir, entry.name); if (entry.isDirectory()) files.push(...walkDir(full)); - else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full); + else if (hasScannableExtension(entry.name)) files.push(full); } return files; } @@ -194,6 +203,7 @@ export { SKIP_DIRS, SCANNABLE_EXTENSIONS, HTML_EXTENSIONS, + hasScannableExtension, walkDir, resolveImport, buildImportGraph, 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/concept-catalog.mjs b/plugin/skills/impeccable/scripts/lib/concept-catalog.mjs index 9c20711ef..949594d0d 100644 --- a/plugin/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/plugin/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes || concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) { errors.push(`concept ${id} must have exactly three structural tags`); } + // The slop this world in particular is at risk of. Optional, because 541 + // entries predate it and none of them are wrong for lacking it. A world built + // from posters is at risk of shouting and one built from instruments is at + // risk of dead greys; a global detector cannot know which, and the author can. + if (concept?.avoid !== undefined) { + if (!Array.isArray(concept.avoid) + || concept.avoid.length < 2 + || concept.avoid.length > 3 + || concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) { + errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`); + } + } if (!Array.isArray(concept?.system) || concept.system.length !== SYSTEM_PREFIXES.length || concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) { 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/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/open-system-browser.mjs b/plugin/skills/impeccable/scripts/lib/open-system-browser.mjs new file mode 100644 index 000000000..c44cd847a --- /dev/null +++ b/plugin/skills/impeccable/scripts/lib/open-system-browser.mjs @@ -0,0 +1,26 @@ +import { spawn } from 'node:child_process'; + +export function browserOpenCommand(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', +} = {}) { + if (platform === 'darwin') return { command: 'open', args: [url] }; + if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] }; + return { command: 'xdg-open', args: [url] }; +} + +export function openSystemBrowser(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', + spawnImpl = spawn, +} = {}) { + const { command, args } = browserOpenCommand(url, { platform, comspec }); + try { + const child = spawnImpl(command, args, { stdio: 'ignore', detached: true }); + child.on('error', () => {}); + child.unref(); + return true; + } catch { + return false; + } +} diff --git a/plugin/skills/impeccable/scripts/lib/roll-selection.mjs b/plugin/skills/impeccable/scripts/lib/roll-selection.mjs index e3c9efbb8..6fab19396 100644 --- a/plugin/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/plugin/skills/impeccable/scripts/lib/roll-selection.mjs @@ -96,31 +96,38 @@ function* rank(items, input, idFor = item => item.id) { .map(entry => entry.item); } -// Two independent exclusions, and either one is enough to hold a world back. -// Rating grades quality: a 3-star earns a second ticket, a 1-star marginal keep -// leaves the pool. Breadth says whether a world can serve an arbitrary build at -// all, so a niche world leaves however good it is, keeping its approval for -// direct briefs. Breadth was split out of rating because the only way to hold a -// narrow world back used to be calling it marginal, which made "excellent but -// narrow" unrecordable and corrupted ratings as a calibration signal. +// Rating sets how many tickets a world holds; breadth decides whether it draws +// at all. A niche world leaves the pool however good it is, keeping its approval +// for direct briefs. Breadth was split out of rating because the only way to +// hold a narrow world back used to be calling it marginal, which made "excellent +// but narrow" unrecordable and corrupted ratings as a calibration signal. +// +// Two tickets for a 3-star, one for everything else, was too sharp. Measured +// against the catalog as it stood: 3-star worlds absorbed 57% of the graphic +// draw from 65 of 163 eligible worlds, 46% of atmosphere from 13 of 43, and +// 75% of interaction from 15 of 25. The reviewer's complaint, that the same +// worlds keep coming back, is what a rating multiplier does to a pool whose +// thinnest tier holds 25 worlds. +// +// So a 3-star no longer outdraws a 2-star, and a 1-star draws at half rather +// than not at all. A marginal keep is still worth showing sometimes: the +// judgement it records is "narrow or unexceptional", not "wrong", and excluding +// it entirely made a rating do a job breadth already does properly. +const RATING_TICKETS = { 1: 1, 2: 2, 3: 2 }; +const ticketsForRating = rating => RATING_TICKETS[rating] ?? 2; + function challengerTickets(pool) { return pool.flatMap(concept => { - const rating = concept.review?.rating; - if (rating === 1 || concept.review?.breadth === 'niche') return []; - return rating === 3 - ? [{ concept, ticket: 0 }, { concept, ticket: 1 }] - : [{ concept, ticket: 0 }]; + if (concept.review?.breadth === 'niche') return []; + return Array.from({ length: ticketsForRating(concept.review?.rating) }, + (_, ticket) => ({ concept, ticket })); }); } function compositionTickets(pool) { - return pool.flatMap(composition => { - const rating = composition.review?.rating; - if (rating === 1) return []; - return rating === 3 - ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] - : [{ composition, ticket: 0 }]; - }); + return pool.flatMap(composition => Array.from( + { length: ticketsForRating(composition.review?.rating) }, + (_, ticket) => ({ composition, ticket }))); } /** 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-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.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 || ''; } } 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 `; } @@ -943,22 +1118,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; } @@ -976,8 +1158,7 @@ server.listen(portArg, '127.0.0.1', () => { console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...'); } if (!hasFlag('no-open')) { - const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'; - try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ } + openSystemBrowser(url); } if (timeoutSec > 0) { setTimeout(() => { 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/scripts/test-suites.mjs b/scripts/test-suites.mjs index 9bb675c1f..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', @@ -162,6 +163,7 @@ export const SUITES = { 'tests/live-svelte-component-accept.test.mjs', 'tests/live-tanstack-adapter.test.mjs', 'tests/live-target-context.test.mjs', + 'tests/live-ui-surfaces.test.mjs', 'tests/live-wrap.test.mjs', 'tests/live-wrap-buffer-aware.test.mjs', ], diff --git a/skill/SKILL.src.md b/skill/SKILL.src.md index 59e56f09c..d78d5190e 100644 --- a/skill/SKILL.src.md +++ b/skill/SKILL.src.md @@ -14,11 +14,11 @@ 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 -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-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..e3524b1a0 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); 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 -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. @@ -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/android.md b/skill/reference/android.md index ae154cba1..a04ba29b5 100644 --- a/skill/reference/android.md +++ b/skill/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/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/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/craft-floor.md b/skill/reference/craft-floor.md index 7cfd23d73..37adc0b77 100644 --- a/skill/reference/craft-floor.md +++ b/skill/reference/craft-floor.md @@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe - **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows. - **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth. - **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus. +- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably. - **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery. - **Coverage:** every brief requirement present and findable within seconds. 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 32901ebe1..f9b785dcd 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/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. 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,13 +84,13 @@ 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 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. ## 6. Build with full commitment -When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the comp at identical dimensions after every region, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. +When an approved comp exists, the comp is king, and the build happens in phases. Phase one is reproduction: rebuild the comp at its own breakpoint until a screenshot at the comp's width and height overlaps it near pixel-perfectly, materials, components, elevation, assets, and implied design language included. Exactly three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user already chose an icon library), and genuine defects in the generated comp such as spelling errors. Everything else must match, and models systematically believe their HTML, CSS, and SVG recreation succeeded when it did not, so the overlap comparison is the authority, never your conviction: set the screenshot beside the freshly reopened comp image at identical dimensions after every region, never beside your memory of it, and when a region keeps losing that comparison, stop recreating it in code and produce it as a rendered asset composited into the page. The comp also outranks every written record of it: when the recorded brief or inventory commits to less than the comp shows, a softer texture, a sparser field, a sculpted plate reduced to flat CSS, correct the record upward to the comp; qualifiers like subtle, restrained, and low-contrast, and counts rounded down to a comfortable fraction, are how approved materials die between approval and build. A produced material must then survive to the screen: a texture buried under a nearly opaque color wash ships the wash, not the material, so judge every material by the screenshot beside the comp, never by the stylesheet. Only when reproduction holds does phase two begin: static regions that should live become animated or interactive, reveals and motion are added, then responsiveness across the surface's devices. Where the comp does not cover the whole surface, continue building the remainder inside the comp's recorded world and design language; a component the comp never shows inherits the recorded system's corner language, line weights, and materials, and may not introduce container styles, border weights, or chrome the comp never uses. Build the assigned direction, not a safer interpretation of it. The form supplies structure, reading order, component conventions, and native motion; the product supplies every fact. Commit every atom: nav, buttons, inputs, and links are rebuilt in the form's vocabulary, and a stock component inside a committed form is a lapse. Land the first build fully committed; committing is the hard part, and the passes that follow exist to make the committed thing clear and effective, never to dilute it. In unattended work, the safe rendition is the known risk. @@ -105,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 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. 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. 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. diff --git a/skill/reference/visualize.md b/skill/reference/visualize.md index c6402a054..09dc93182 100644 --- a/skill/reference/visualize.md +++ b/skill/reference/visualize.md @@ -1,12 +1,12 @@ # 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. ## 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/concept-seed.mjs b/skill/scripts/concept-seed.mjs index aab9e8911..db638ab57 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, @@ -553,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/skill/scripts/context-signals.mjs b/skill/scripts/context-signals.mjs index 743bb220a..e56214be1 100644 --- a/skill/scripts/context-signals.mjs +++ b/skill/scripts/context-signals.mjs @@ -22,7 +22,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { execFileSync } from 'node:child_process'; import { loadContext, extractPlatform } from './context.mjs'; -import { getCritiqueDir } from './lib/impeccable-paths.mjs'; +import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs'; /** Is there code here at all, or just context files / an empty repo? */ function hasCode(cwd) { @@ -34,23 +34,13 @@ function hasCode(cwd) { } /** - * The most recent critique snapshot across all targets. Filenames are - * timestamp-prefixed (`__.md`), so a lexical sort is chronological. - * Parses the small frontmatter for score + P0/P1 counts. + * Summarize the most recent critique snapshot across all targets. */ function latestCritique(cwd) { try { - const dir = getCritiqueDir(cwd); - if (!fs.existsSync(dir)) return null; - const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort(); - if (!files.length) return null; - const newest = files[files.length - 1]; - const text = fs.readFileSync(path.join(dir, newest), 'utf-8'); - const front = text.split('---')[1] || ''; - const get = (k) => { - const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm')); - return m ? m[1].trim() : null; - }; + const latest = readLatestSnapshotAcrossTargets({ cwd }); + if (!latest) return null; + const get = (key) => latest.meta[key] ?? null; const num = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null; @@ -61,7 +51,7 @@ function latestCritique(cwd) { p0: num(get('p0')), p1: num(get('p1')), timestamp: get('timestamp'), - file: path.relative(cwd, path.join(dir, newest)), + file: path.relative(cwd, latest.path), }; } catch { return null; diff --git a/skill/scripts/critique-storage.mjs b/skill/scripts/critique-storage.mjs index a8b36b025..f23fded37 100644 --- a/skill/scripts/critique-storage.mjs +++ b/skill/scripts/critique-storage.mjs @@ -105,28 +105,37 @@ function parseFrontmatter(text) { } /** - * Return all snapshot files for `slug`, sorted oldest → newest. + * Return snapshot files matching `suffix`, sorted oldest → newest. */ -function listSnapshotsForSlug(slug, cwd) { +const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/; + +function listSnapshots(suffix, cwd) { const dir = getCritiqueDir(cwd); if (!fs.existsSync(dir)) return []; - const suffix = `__${slug}.md`; return fs.readdirSync(dir) - .filter((f) => f.endsWith(suffix)) + .filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix)) .sort() .map((f) => path.join(dir, f)); } +function readLatestSnapshotMatching(suffix, cwd) { + const filePath = listSnapshots(suffix, cwd).at(-1); + if (!filePath) return null; + const body = fs.readFileSync(filePath, 'utf-8'); + return { path: filePath, body, meta: parseFrontmatter(body) }; +} + /** * Return the most recent snapshot for `slug`, or null. Polish reads this * to find its fix backlog when the slug matches. */ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); - if (!all.length) return null; - const latest = all[all.length - 1]; - const body = fs.readFileSync(latest, 'utf-8'); - return { path: latest, body, meta: parseFrontmatter(body) }; + return readLatestSnapshotMatching(`__${slug}.md`, cwd); +} + +/** Return the most recent snapshot across all targets, or null. */ +export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) { + return readLatestSnapshotMatching('.md', cwd); } /** @@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) { * Critique appends a one-line trend to its output using this. */ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) { - const all = listSnapshotsForSlug(slug, cwd); + const all = listSnapshots(`__${slug}.md`, cwd); const slice = all.slice(-limit); return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8'))); } diff --git a/skill/scripts/hook-lib.mjs b/skill/scripts/hook-lib.mjs index 4768c7c87..b5e4570b1 100644 --- a/skill/scripts/hook-lib.mjs +++ b/skill/scripts/hook-lib.mjs @@ -1170,7 +1170,19 @@ function formatFindingIgnoreHint(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/concept-catalog.mjs b/skill/scripts/lib/concept-catalog.mjs index 9c20711ef..949594d0d 100644 --- a/skill/scripts/lib/concept-catalog.mjs +++ b/skill/scripts/lib/concept-catalog.mjs @@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes || concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) { errors.push(`concept ${id} must have exactly three structural tags`); } + // The slop this world in particular is at risk of. Optional, because 541 + // entries predate it and none of them are wrong for lacking it. A world built + // from posters is at risk of shouting and one built from instruments is at + // risk of dead greys; a global detector cannot know which, and the author can. + if (concept?.avoid !== undefined) { + if (!Array.isArray(concept.avoid) + || concept.avoid.length < 2 + || concept.avoid.length > 3 + || concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) { + errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`); + } + } if (!Array.isArray(concept?.system) || concept.system.length !== SYSTEM_PREFIXES.length || concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) { 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/open-system-browser.mjs b/skill/scripts/lib/open-system-browser.mjs new file mode 100644 index 000000000..c44cd847a --- /dev/null +++ b/skill/scripts/lib/open-system-browser.mjs @@ -0,0 +1,26 @@ +import { spawn } from 'node:child_process'; + +export function browserOpenCommand(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', +} = {}) { + if (platform === 'darwin') return { command: 'open', args: [url] }; + if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] }; + return { command: 'xdg-open', args: [url] }; +} + +export function openSystemBrowser(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', + spawnImpl = spawn, +} = {}) { + const { command, args } = browserOpenCommand(url, { platform, comspec }); + try { + const child = spawnImpl(command, args, { stdio: 'ignore', detached: true }); + child.on('error', () => {}); + child.unref(); + return true; + } catch { + return false; + } +} diff --git a/skill/scripts/lib/roll-selection.mjs b/skill/scripts/lib/roll-selection.mjs index e3c9efbb8..6fab19396 100644 --- a/skill/scripts/lib/roll-selection.mjs +++ b/skill/scripts/lib/roll-selection.mjs @@ -96,31 +96,38 @@ function* rank(items, input, idFor = item => item.id) { .map(entry => entry.item); } -// Two independent exclusions, and either one is enough to hold a world back. -// Rating grades quality: a 3-star earns a second ticket, a 1-star marginal keep -// leaves the pool. Breadth says whether a world can serve an arbitrary build at -// all, so a niche world leaves however good it is, keeping its approval for -// direct briefs. Breadth was split out of rating because the only way to hold a -// narrow world back used to be calling it marginal, which made "excellent but -// narrow" unrecordable and corrupted ratings as a calibration signal. +// Rating sets how many tickets a world holds; breadth decides whether it draws +// at all. A niche world leaves the pool however good it is, keeping its approval +// for direct briefs. Breadth was split out of rating because the only way to +// hold a narrow world back used to be calling it marginal, which made "excellent +// but narrow" unrecordable and corrupted ratings as a calibration signal. +// +// Two tickets for a 3-star, one for everything else, was too sharp. Measured +// against the catalog as it stood: 3-star worlds absorbed 57% of the graphic +// draw from 65 of 163 eligible worlds, 46% of atmosphere from 13 of 43, and +// 75% of interaction from 15 of 25. The reviewer's complaint, that the same +// worlds keep coming back, is what a rating multiplier does to a pool whose +// thinnest tier holds 25 worlds. +// +// So a 3-star no longer outdraws a 2-star, and a 1-star draws at half rather +// than not at all. A marginal keep is still worth showing sometimes: the +// judgement it records is "narrow or unexceptional", not "wrong", and excluding +// it entirely made a rating do a job breadth already does properly. +const RATING_TICKETS = { 1: 1, 2: 2, 3: 2 }; +const ticketsForRating = rating => RATING_TICKETS[rating] ?? 2; + function challengerTickets(pool) { return pool.flatMap(concept => { - const rating = concept.review?.rating; - if (rating === 1 || concept.review?.breadth === 'niche') return []; - return rating === 3 - ? [{ concept, ticket: 0 }, { concept, ticket: 1 }] - : [{ concept, ticket: 0 }]; + if (concept.review?.breadth === 'niche') return []; + return Array.from({ length: ticketsForRating(concept.review?.rating) }, + (_, ticket) => ({ concept, ticket })); }); } function compositionTickets(pool) { - return pool.flatMap(composition => { - const rating = composition.review?.rating; - if (rating === 1) return []; - return rating === 3 - ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] - : [{ composition, ticket: 0 }]; - }); + return pool.flatMap(composition => Array.from( + { length: ticketsForRating(composition.review?.rating) }, + (_, ticket) => ({ composition, ticket }))); } /** 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-browser.js b/skill/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/skill/scripts/live-browser.js +++ b/skill/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/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/skill/scripts/live/browser-script-parts.mjs b/skill/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/skill/scripts/live/browser-script-parts.mjs +++ b/skill/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/skill/scripts/live/ui-surfaces.mjs b/skill/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/skill/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 `; } @@ -943,22 +1118,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; } @@ -976,8 +1158,7 @@ server.listen(portArg, '127.0.0.1', () => { console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...'); } if (!hasFlag('no-open')) { - const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'; - try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ } + openSystemBrowser(url); } if (timeoutSec > 0) { setTimeout(() => { 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(/