From ca7981f669d636f8dfbf13c2f60a95c4ca965b51 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Mon, 3 Aug 2026 08:47:30 -0700 Subject: [PATCH 01/35] Comp outranks the brief: close the inventory sandbagging gap probe-compking-sol-4 (evals) executed its staged inventory faithfully and still lost the comp: the brief had already recorded the comp's materials down (low-contrast textures, a 70-path lake against the comp's hundreds, a sculpted plate as flat CSS), and the build thread never loads visualize.md, so nothing told it the comp wins that disagreement. The comp-is-king block now says the record gets corrected upward, that the comparison runs against the freshly reopened comp rather than memory, and that a texture under a near-opaque wash is not shipped material. Co-Authored-By: Claude Fable 5 --- skill/reference/new-work.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skill/reference/new-work.md b/skill/reference/new-work.md index 32901ebe1..20a956129 100644 --- a/skill/reference/new-work.md +++ b/skill/reference/new-work.md @@ -88,7 +88,7 @@ For `shape`, return the selected direction to [shape.md](shape.md) and stop befo ## 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. From 3b35161000ebf5ee97ced9c0d2c304efa3b22b63 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Tue, 4 Aug 2026 10:31:40 -0700 Subject: [PATCH 02/35] Flatten the challenger draw so the same worlds stop coming back A 3-star held two tickets and a 1-star held none. On a pool this size that is not a nudge, it is the shape of the draw. Measured against the live catalog: 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 report that the same worlds keep returning is exactly what a rating multiplier does to a corpus whose thinnest tier holds 25 worlds. Now a 3-star draws level with a 2-star, and a 1-star draws at half rather than not at all. Excluding a marginal keep made rating do a job breadth already does properly: breadth still removes a niche world from the pool entirely, which is the honest way to say "too narrow to challenge an arbitrary build", while a 1-star records "unexceptional" and is still worth showing sometimes. Effect on the same catalog: the 3-star share falls to 39% on graphic, 30% on atmosphere and 60% on interaction. That last one is no longer a weighting artefact, it is simply what the tier contains, since 15 of its 25 eligible worlds are rated 3. Compositions get the same treatment; the two ticket functions had the identical shape and no reason to disagree. Both tests asserted the old policy directly and now assert the new one. Co-Authored-By: Claude Opus 5 (1M context) --- skill/scripts/lib/roll-selection.mjs | 45 ++++++++++++++++------------ tests/concept-seed.test.mjs | 32 +++++++++++++------- 2 files changed, 48 insertions(+), 29 deletions(-) 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/tests/concept-seed.test.mjs b/tests/concept-seed.test.mjs index 125550ce8..2717dd849 100644 --- a/tests/concept-seed.test.mjs +++ b/tests/concept-seed.test.mjs @@ -314,11 +314,19 @@ describe('concept seed scopes', () => { const graphicFirst = picks.find(pick => pick.wellTier === 'graphic'); counts[graphicFirst.id] += 1; } - assert.equal(counts.marginal, 0); - // Two tickets should put the flagship on top roughly twice as often as an - // unrated peer; a generous margin keeps the assertion deterministic-safe. - assert.equal(counts.flagship > counts['solid-b'] * 1.3, true, - `flagship ${counts.flagship} vs solid-b ${counts['solid-b']}`); + // A 1-star draws at half weight rather than not at all. Excluding it made a + // rating do the job breadth already does, and a marginal keep records + // "narrow or unexceptional" rather than "wrong". + assert.equal(counts.marginal > 0, true, `marginal ${counts.marginal} should draw`); + assert.equal(counts.marginal < counts['solid-b'], true, + `marginal ${counts.marginal} should draw below solid-b ${counts['solid-b']}`); + + // A 3-star no longer outdraws a 2-star. The multiplier concentrated the + // draw hard on a thin pool: measured on the live catalog, 3-star worlds took + // 75% of the interaction draw from 15 of 25 eligible worlds. + const spread = Math.abs(counts.flagship - counts['solid-b']) / Math.max(counts.flagship, counts['solid-b']); + assert.equal(spread < 0.4, true, + `flagship ${counts.flagship} and solid-b ${counts['solid-b']} should draw comparably`); // A tier holding only 1-star approvals still yields challengers. const onlyMarginal = [ @@ -380,11 +388,15 @@ describe('concept seed scopes', () => { const picks = selectApprovedCompositions({ scope: 'direction', key: `stage-weight-${index}`, mode: 'persuade', sourceCompositions: pool, count: 1 }); counts[picks[0].id] += 1; } - assert.equal(counts['marginal-stage'], 0, 'a 1-star composition keeps its approval but leaves the draw'); - // Two tickets should put the flagship first roughly twice as often as the - // unrated peer; a generous margin keeps the assertion deterministic-safe. - assert.equal(counts['flagship-stage'] > counts['plain-stage'] * 1.3, true, - `flagship ${counts['flagship-stage']} vs plain ${counts['plain-stage']}`); + // Same weighting as challengers: a 1-star draws at half rather than not at + // all, and a 3-star no longer outdraws a 2-star. + assert.equal(counts['marginal-stage'] > 0, true, 'a 1-star composition still draws, at half weight'); + assert.equal(counts['marginal-stage'] < counts['plain-stage'], true, + `marginal ${counts['marginal-stage']} should draw below plain ${counts['plain-stage']}`); + const stageSpread = Math.abs(counts['flagship-stage'] - counts['plain-stage']) + / Math.max(counts['flagship-stage'], counts['plain-stage']); + assert.equal(stageSpread < 0.4, true, + `flagship ${counts['flagship-stage']} and plain ${counts['plain-stage']} should draw comparably`); // A pool of nothing but 1-star keeps still yields compositions. const onlyMarginal = [ From e15d8e122f6ce4d95ca3846646da7f82430a29a6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 18:30:42 +0000 Subject: [PATCH 03/35] Sync generated provider output --- .../skills/impeccable/reference/new-work.md | 2 +- .../impeccable/scripts/lib/roll-selection.mjs | 45 +++++++++++-------- .../skills/impeccable/reference/new-work.md | 2 +- .../impeccable/scripts/lib/roll-selection.mjs | 45 +++++++++++-------- .../skills/impeccable/reference/new-work.md | 2 +- .../impeccable/scripts/lib/roll-selection.mjs | 45 +++++++++++-------- .../skills/impeccable/reference/new-work.md | 2 +- .../impeccable/scripts/lib/roll-selection.mjs | 45 +++++++++++-------- .../skills/impeccable/reference/new-work.md | 2 +- .../impeccable/scripts/lib/roll-selection.mjs | 45 +++++++++++-------- .grok/skills/impeccable/reference/new-work.md | 2 +- .../impeccable/scripts/lib/roll-selection.mjs | 45 +++++++++++-------- .kiro/skills/impeccable/reference/new-work.md | 2 +- .../impeccable/scripts/lib/roll-selection.mjs | 45 +++++++++++-------- .../skills/impeccable/reference/new-work.md | 2 +- .../impeccable/scripts/lib/roll-selection.mjs | 45 +++++++++++-------- .pi/skills/impeccable/reference/new-work.md | 2 +- .../impeccable/scripts/lib/roll-selection.mjs | 45 +++++++++++-------- .../skills/impeccable/reference/new-work.md | 2 +- .../impeccable/scripts/lib/roll-selection.mjs | 45 +++++++++++-------- .../skills/impeccable/reference/new-work.md | 2 +- .../impeccable/scripts/lib/roll-selection.mjs | 45 +++++++++++-------- .../skills/impeccable/reference/new-work.md | 2 +- .../impeccable/scripts/lib/roll-selection.mjs | 45 +++++++++++-------- .trae/skills/impeccable/reference/new-work.md | 2 +- .../impeccable/scripts/lib/roll-selection.mjs | 45 +++++++++++-------- .vibe/skills/impeccable/reference/new-work.md | 2 +- .../impeccable/scripts/lib/roll-selection.mjs | 45 +++++++++++-------- .../skills/impeccable/reference/new-work.md | 2 +- .../impeccable/scripts/lib/roll-selection.mjs | 45 +++++++++++-------- 30 files changed, 405 insertions(+), 300 deletions(-) diff --git a/.agents/skills/impeccable/reference/new-work.md b/.agents/skills/impeccable/reference/new-work.md index fa1fb24dd..755d734e3 100644 --- a/.agents/skills/impeccable/reference/new-work.md +++ b/.agents/skills/impeccable/reference/new-work.md @@ -84,7 +84,7 @@ For `shape`, return the selected direction to [shape.md](shape.md) and stop befo ## 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. 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/.claude/skills/impeccable/reference/new-work.md b/.claude/skills/impeccable/reference/new-work.md index 0ea9a6d21..5161d25cb 100644 --- a/.claude/skills/impeccable/reference/new-work.md +++ b/.claude/skills/impeccable/reference/new-work.md @@ -86,7 +86,7 @@ For `shape`, return the selected direction to [shape.md](shape.md) and stop befo ## 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. 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/.cursor/skills/impeccable/reference/new-work.md b/.cursor/skills/impeccable/reference/new-work.md index 0158637bd..99ed558b5 100644 --- a/.cursor/skills/impeccable/reference/new-work.md +++ b/.cursor/skills/impeccable/reference/new-work.md @@ -84,7 +84,7 @@ For `shape`, return the selected direction to [shape.md](shape.md) and stop befo ## 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. 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/.gemini/skills/impeccable/reference/new-work.md b/.gemini/skills/impeccable/reference/new-work.md index a09b2e68a..77ec7396f 100644 --- a/.gemini/skills/impeccable/reference/new-work.md +++ b/.gemini/skills/impeccable/reference/new-work.md @@ -84,7 +84,7 @@ For `shape`, return the selected direction to [shape.md](shape.md) and stop befo ## 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. 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/.github/skills/impeccable/reference/new-work.md b/.github/skills/impeccable/reference/new-work.md index a897e92d2..a249c27ad 100644 --- a/.github/skills/impeccable/reference/new-work.md +++ b/.github/skills/impeccable/reference/new-work.md @@ -84,7 +84,7 @@ For `shape`, return the selected direction to [shape.md](shape.md) and stop befo ## 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. 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/.grok/skills/impeccable/reference/new-work.md b/.grok/skills/impeccable/reference/new-work.md index 98a628227..45027be6e 100644 --- a/.grok/skills/impeccable/reference/new-work.md +++ b/.grok/skills/impeccable/reference/new-work.md @@ -84,7 +84,7 @@ For `shape`, return the selected direction to [shape.md](shape.md) and stop befo ## 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. 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/.kiro/skills/impeccable/reference/new-work.md b/.kiro/skills/impeccable/reference/new-work.md index d716b4359..479e37b10 100644 --- a/.kiro/skills/impeccable/reference/new-work.md +++ b/.kiro/skills/impeccable/reference/new-work.md @@ -84,7 +84,7 @@ For `shape`, return the selected direction to [shape.md](shape.md) and stop befo ## 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. 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/.opencode/skills/impeccable/reference/new-work.md b/.opencode/skills/impeccable/reference/new-work.md index 531bd490c..290c53138 100644 --- a/.opencode/skills/impeccable/reference/new-work.md +++ b/.opencode/skills/impeccable/reference/new-work.md @@ -84,7 +84,7 @@ For `shape`, return the selected direction to [shape.md](shape.md) and stop befo ## 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. 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/.pi/skills/impeccable/reference/new-work.md b/.pi/skills/impeccable/reference/new-work.md index 4964ab781..57437e7ba 100644 --- a/.pi/skills/impeccable/reference/new-work.md +++ b/.pi/skills/impeccable/reference/new-work.md @@ -84,7 +84,7 @@ For `shape`, return the selected direction to [shape.md](shape.md) and stop befo ## 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. 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/.qoder/skills/impeccable/reference/new-work.md b/.qoder/skills/impeccable/reference/new-work.md index fb8ddb923..983ce2b96 100644 --- a/.qoder/skills/impeccable/reference/new-work.md +++ b/.qoder/skills/impeccable/reference/new-work.md @@ -84,7 +84,7 @@ For `shape`, return the selected direction to [shape.md](shape.md) and stop befo ## 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. 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/.rovodev/skills/impeccable/reference/new-work.md b/.rovodev/skills/impeccable/reference/new-work.md index 6a62f6715..9b3edee90 100644 --- a/.rovodev/skills/impeccable/reference/new-work.md +++ b/.rovodev/skills/impeccable/reference/new-work.md @@ -84,7 +84,7 @@ For `shape`, return the selected direction to [shape.md](shape.md) and stop befo ## 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. 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/.trae-cn/skills/impeccable/reference/new-work.md b/.trae-cn/skills/impeccable/reference/new-work.md index 8115cd9d9..f47a72b20 100644 --- a/.trae-cn/skills/impeccable/reference/new-work.md +++ b/.trae-cn/skills/impeccable/reference/new-work.md @@ -84,7 +84,7 @@ For `shape`, return the selected direction to [shape.md](shape.md) and stop befo ## 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. 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/skills/impeccable/reference/new-work.md b/.trae/skills/impeccable/reference/new-work.md index 1d8c11bac..f70c82584 100644 --- a/.trae/skills/impeccable/reference/new-work.md +++ b/.trae/skills/impeccable/reference/new-work.md @@ -84,7 +84,7 @@ For `shape`, return the selected direction to [shape.md](shape.md) and stop befo ## 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. 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/.vibe/skills/impeccable/reference/new-work.md b/.vibe/skills/impeccable/reference/new-work.md index 620301c57..bda245059 100644 --- a/.vibe/skills/impeccable/reference/new-work.md +++ b/.vibe/skills/impeccable/reference/new-work.md @@ -84,7 +84,7 @@ For `shape`, return the selected direction to [shape.md](shape.md) and stop befo ## 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. 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/plugin/skills/impeccable/reference/new-work.md b/plugin/skills/impeccable/reference/new-work.md index 0ea9a6d21..5161d25cb 100644 --- a/plugin/skills/impeccable/reference/new-work.md +++ b/plugin/skills/impeccable/reference/new-work.md @@ -86,7 +86,7 @@ For `shape`, return the selected direction to [shape.md](shape.md) and stop befo ## 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. 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 }))); } /** From d417ff1f015f1fb77ce955efe99e655916f96cf9 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Tue, 4 Aug 2026 12:23:08 -0700 Subject: [PATCH 04/35] Craft floor: theme the surfaces you did not draw A well-made site was audited for what separates it from a competent one, and the answer was not its ingredients. It runs the default stack, Next and Tailwind and Geist, with no world and no unusual technique. What it has is attention to the surfaces a browser renders for you: 29 focus-visible rules, 15 scrollbar rules, and styled text selection, caret, underline offset and scroll behaviour. Those are the cheapest signal that a page was built rather than assembled, and the ones a model skips most reliably, because nobody asks for them and nothing looks broken without them. The floor already covers contrast, depth, spacing, measure, motion and states; this is the layer under all of it. Co-Authored-By: Claude Opus 5 (1M context) --- skill/reference/craft-floor.md | 1 + 1 file changed, 1 insertion(+) 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. From ebaf9f1d5b649b4e4ddad992a81e5302282df26d Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Tue, 4 Aug 2026 13:31:59 -0700 Subject: [PATCH 05/35] Let a world declare the slop it is personally at risk of Optional `avoid`, two or three negations of 12 to 160 characters. A world built from posters is at risk of shouting; one built from instruments is at risk of dead greys. The global detector cannot know which and the author can, so the "do not" belongs beside the "do" rather than in a rulebook that applies to everything equally. Optional on purpose: 541 entries predate it and none of them are wrong for lacking it, so nothing needs backfilling. Co-Authored-By: Claude Opus 5 (1M context) --- skill/scripts/lib/concept-catalog.mjs | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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)) { From 2f609915eb93c0a2ebe02884a02029d244a674ae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 20:34:25 +0000 Subject: [PATCH 06/35] Sync generated provider output --- .agents/skills/impeccable/reference/craft-floor.md | 1 + .../impeccable/scripts/lib/concept-catalog.mjs | 12 ++++++++++++ .claude/skills/impeccable/reference/craft-floor.md | 1 + .../impeccable/scripts/lib/concept-catalog.mjs | 12 ++++++++++++ .cursor/skills/impeccable/reference/craft-floor.md | 1 + .../impeccable/scripts/lib/concept-catalog.mjs | 12 ++++++++++++ .gemini/skills/impeccable/reference/craft-floor.md | 1 + .../impeccable/scripts/lib/concept-catalog.mjs | 12 ++++++++++++ .github/skills/impeccable/reference/craft-floor.md | 1 + .../impeccable/scripts/lib/concept-catalog.mjs | 12 ++++++++++++ .grok/skills/impeccable/reference/craft-floor.md | 1 + .../impeccable/scripts/lib/concept-catalog.mjs | 12 ++++++++++++ .kiro/skills/impeccable/reference/craft-floor.md | 1 + .../impeccable/scripts/lib/concept-catalog.mjs | 12 ++++++++++++ .opencode/skills/impeccable/reference/craft-floor.md | 1 + .../impeccable/scripts/lib/concept-catalog.mjs | 12 ++++++++++++ .pi/skills/impeccable/reference/craft-floor.md | 1 + .../impeccable/scripts/lib/concept-catalog.mjs | 12 ++++++++++++ .qoder/skills/impeccable/reference/craft-floor.md | 1 + .../impeccable/scripts/lib/concept-catalog.mjs | 12 ++++++++++++ .rovodev/skills/impeccable/reference/craft-floor.md | 1 + .../impeccable/scripts/lib/concept-catalog.mjs | 12 ++++++++++++ .trae-cn/skills/impeccable/reference/craft-floor.md | 1 + .../impeccable/scripts/lib/concept-catalog.mjs | 12 ++++++++++++ .trae/skills/impeccable/reference/craft-floor.md | 1 + .../impeccable/scripts/lib/concept-catalog.mjs | 12 ++++++++++++ .vibe/skills/impeccable/reference/craft-floor.md | 1 + .../impeccable/scripts/lib/concept-catalog.mjs | 12 ++++++++++++ plugin/skills/impeccable/reference/craft-floor.md | 1 + .../impeccable/scripts/lib/concept-catalog.mjs | 12 ++++++++++++ 30 files changed, 195 insertions(+) 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/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/.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/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/.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/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/.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/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/.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/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/.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/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/.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/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/.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/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/.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/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/.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/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/.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/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/.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/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/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/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/.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/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/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/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)) { From 80e4dd0d581fcdb42be62252b7bc07dcd2238330 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Tue, 4 Aug 2026 14:08:49 -0700 Subject: [PATCH 07/35] Fix Blade files in directory detection (#509) * Fix Blade directory detection AI assistance: Codex reproduced the issue, implemented the fix, and ran the validation described in the pull request. * Fix compound scan suffix matching AI assistance: Codex addressed review findings and ran the validation described in the pull request. --- cli/engine/detect-antipatterns.mjs | 1 + cli/engine/node/file-system.mjs | 14 ++++++++++++-- tests/detect-antipatterns.test.js | 26 ++++++++++++++++++++++++-- 3 files changed, 37 insertions(+), 4 deletions(-) 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/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/tests/detect-antipatterns.test.js b/tests/detect-antipatterns.test.js index bfc84fab8..f5afc97b6 100644 --- a/tests/detect-antipatterns.test.js +++ b/tests/detect-antipatterns.test.js @@ -6,7 +6,7 @@ import { spawnSync } from 'child_process'; import { ANTIPATTERNS, checkElementBorders, checkElementMotion, checkElementGlow, isNeutralColor, isFullPage, detectText, detectHtml, extractStyleBlocks, extractCSSinJS, - walkDir, SCANNABLE_EXTENSIONS, + walkDir, hasScannableExtension, SCANNABLE_EXTENSIONS, buildImportGraph, resolveImport, detectFrameworkConfig, isPortListening, FRAMEWORK_CONFIGS, } from '../cli/engine/detect-antipatterns.mjs'; @@ -2071,10 +2071,32 @@ describe('walkDir', () => { expect(SCANNABLE_EXTENSIONS.has('.sass')).toBe(true); }); + test('finds Blade templates during directory scans', () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-walk-')); + try { + const blade = path.join(tmp, 'resources', 'views', 'card.blade.php'); + const upperBlade = path.join(tmp, 'resources', 'views', 'hero.BLADE.PHP'); + fs.mkdirSync(path.dirname(blade), { recursive: true }); + fs.writeFileSync(blade, '
Card
'); + fs.writeFileSync(upperBlade, '
Hero
'); + expect(walkDir(tmp)).toEqual(expect.arrayContaining([blade, upperBlade])); + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } + }); + + test('compound suffix matching does not broaden scans to module extensions', () => { + expect(hasScannableExtension('card.blade.php')).toBe(true); + expect(hasScannableExtension('card.BLADE.PHP')).toBe(true); + for (const file of ['next.config.mjs', 'vite.config.cjs', 'route.mts', 'route.cts']) { + expect(hasScannableExtension(file)).toBe(false); + } + }); + test('finds scannable files', () => { const files = walkDir(FIXTURES); expect(files.length).toBeGreaterThanOrEqual(3); - expect(files.every(f => SCANNABLE_EXTENSIONS.has(path.extname(f)))).toBe(true); + expect(files.every(hasScannableExtension)).toBe(true); }); test('returns empty for nonexistent dir', () => { From d086837dfcc9654743d1b17cbe051532cd3bebcf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 21:09:31 +0000 Subject: [PATCH 08/35] Sync generated provider output --- .../scripts/detector/detect-antipatterns.mjs | 1 + .../scripts/detector/node/file-system.mjs | 14 ++++++++++++-- .../scripts/detector/detect-antipatterns.mjs | 1 + .../scripts/detector/node/file-system.mjs | 14 ++++++++++++-- .../scripts/detector/detect-antipatterns.mjs | 1 + .../scripts/detector/node/file-system.mjs | 14 ++++++++++++-- .../scripts/detector/detect-antipatterns.mjs | 1 + .../scripts/detector/node/file-system.mjs | 14 ++++++++++++-- .../scripts/detector/detect-antipatterns.mjs | 1 + .../scripts/detector/node/file-system.mjs | 14 ++++++++++++-- .../scripts/detector/detect-antipatterns.mjs | 1 + .../scripts/detector/node/file-system.mjs | 14 ++++++++++++-- .../scripts/detector/detect-antipatterns.mjs | 1 + .../scripts/detector/node/file-system.mjs | 14 ++++++++++++-- .../scripts/detector/detect-antipatterns.mjs | 1 + .../scripts/detector/node/file-system.mjs | 14 ++++++++++++-- .../scripts/detector/detect-antipatterns.mjs | 1 + .../scripts/detector/node/file-system.mjs | 14 ++++++++++++-- .../scripts/detector/detect-antipatterns.mjs | 1 + .../scripts/detector/node/file-system.mjs | 14 ++++++++++++-- .../scripts/detector/detect-antipatterns.mjs | 1 + .../scripts/detector/node/file-system.mjs | 14 ++++++++++++-- .../scripts/detector/detect-antipatterns.mjs | 1 + .../scripts/detector/node/file-system.mjs | 14 ++++++++++++-- .../scripts/detector/detect-antipatterns.mjs | 1 + .../scripts/detector/node/file-system.mjs | 14 ++++++++++++-- .../scripts/detector/detect-antipatterns.mjs | 1 + .../scripts/detector/node/file-system.mjs | 14 ++++++++++++-- .../scripts/detector/detect-antipatterns.mjs | 1 + .../scripts/detector/node/file-system.mjs | 14 ++++++++++++-- 30 files changed, 195 insertions(+), 30 deletions(-) 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/.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/.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/.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/.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/.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/.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/.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/.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/.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/.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/.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/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/.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/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, From a37b3f6b02715f6e14d2f6269226143de20c7f17 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Tue, 4 Aug 2026 14:09:59 -0700 Subject: [PATCH 09/35] Fix Windows question browser opening (#510) AI assistance: Codex reproduced the issue, implemented the fix, and ran the validation described in the pull request. --- skill/scripts/lib/open-system-browser.mjs | 26 +++++++++++++++++++++++ skill/scripts/serve-question.mjs | 4 ++-- tests/serve-question.test.mjs | 21 ++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 skill/scripts/lib/open-system-browser.mjs 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/serve-question.mjs b/skill/scripts/serve-question.mjs index 25beaa759..2d8c24a6a 100644 --- a/skill/scripts/serve-question.mjs +++ b/skill/scripts/serve-question.mjs @@ -79,6 +79,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; +import { openSystemBrowser } from './lib/open-system-browser.mjs'; function arg(name, fallback = null) { const i = process.argv.indexOf(`--${name}`); @@ -976,8 +977,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/serve-question.test.mjs b/tests/serve-question.test.mjs index 0371be4a1..cfee001b0 100644 --- a/tests/serve-question.test.mjs +++ b/tests/serve-question.test.mjs @@ -5,6 +5,9 @@ import { writeFileSync, mkdtempSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { EventEmitter } from 'node:events'; + +import { browserOpenCommand, openSystemBrowser } from '../skill/scripts/lib/open-system-browser.mjs'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const SCRIPT = path.join(ROOT, 'skill', 'scripts', 'serve-question.mjs'); @@ -39,6 +42,24 @@ const PAYLOAD = { }; describe('serve-question', () => { + it('opens Windows URLs through cmd.exe and reserves the start title argument', () => { + assert.deepEqual( + browserOpenCommand('http://127.0.0.1:1234/', { platform: 'win32', comspec: 'cmd.exe' }), + { command: 'cmd.exe', args: ['/c', 'start', '', 'http://127.0.0.1:1234/'] }, + ); + }); + + it('absorbs asynchronous system-opener failures after printing the URL', () => { + const child = new EventEmitter(); + child.unref = () => {}; + assert.equal(openSystemBrowser('http://127.0.0.1:1234/', { + platform: 'linux', + spawnImpl: () => child, + }), true); + assert.equal(child.listenerCount('error'), 1); + assert.doesNotThrow(() => child.emit('error', Object.assign(new Error('missing opener'), { code: 'ENOENT' }))); + }); + it('serves the page, records the answer, prints ANSWER, exits 0', async () => { const { child, url, read } = await startServer(PAYLOAD); const html = await (await fetch(url)).text(); From ae5e95101a6979e7f7973a4ff57680b3c7adc1ec Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 21:10:37 +0000 Subject: [PATCH 10/35] Sync generated provider output --- .../scripts/lib/open-system-browser.mjs | 26 +++++++++++++++++++ .../impeccable/scripts/serve-question.mjs | 4 +-- .../scripts/lib/open-system-browser.mjs | 26 +++++++++++++++++++ .../impeccable/scripts/serve-question.mjs | 4 +-- .../scripts/lib/open-system-browser.mjs | 26 +++++++++++++++++++ .../impeccable/scripts/serve-question.mjs | 4 +-- .../scripts/lib/open-system-browser.mjs | 26 +++++++++++++++++++ .../impeccable/scripts/serve-question.mjs | 4 +-- .../scripts/lib/open-system-browser.mjs | 26 +++++++++++++++++++ .../impeccable/scripts/serve-question.mjs | 4 +-- .../scripts/lib/open-system-browser.mjs | 26 +++++++++++++++++++ .../impeccable/scripts/serve-question.mjs | 4 +-- .../scripts/lib/open-system-browser.mjs | 26 +++++++++++++++++++ .../impeccable/scripts/serve-question.mjs | 4 +-- .../scripts/lib/open-system-browser.mjs | 26 +++++++++++++++++++ .../impeccable/scripts/serve-question.mjs | 4 +-- .../scripts/lib/open-system-browser.mjs | 26 +++++++++++++++++++ .../impeccable/scripts/serve-question.mjs | 4 +-- .../scripts/lib/open-system-browser.mjs | 26 +++++++++++++++++++ .../impeccable/scripts/serve-question.mjs | 4 +-- .../scripts/lib/open-system-browser.mjs | 26 +++++++++++++++++++ .../impeccable/scripts/serve-question.mjs | 4 +-- .../scripts/lib/open-system-browser.mjs | 26 +++++++++++++++++++ .../impeccable/scripts/serve-question.mjs | 4 +-- .../scripts/lib/open-system-browser.mjs | 26 +++++++++++++++++++ .../impeccable/scripts/serve-question.mjs | 4 +-- .../scripts/lib/open-system-browser.mjs | 26 +++++++++++++++++++ .../impeccable/scripts/serve-question.mjs | 4 +-- .../scripts/lib/open-system-browser.mjs | 26 +++++++++++++++++++ .../impeccable/scripts/serve-question.mjs | 4 +-- 30 files changed, 420 insertions(+), 30 deletions(-) create mode 100644 .agents/skills/impeccable/scripts/lib/open-system-browser.mjs create mode 100644 .claude/skills/impeccable/scripts/lib/open-system-browser.mjs create mode 100644 .cursor/skills/impeccable/scripts/lib/open-system-browser.mjs create mode 100644 .gemini/skills/impeccable/scripts/lib/open-system-browser.mjs create mode 100644 .github/skills/impeccable/scripts/lib/open-system-browser.mjs create mode 100644 .grok/skills/impeccable/scripts/lib/open-system-browser.mjs create mode 100644 .kiro/skills/impeccable/scripts/lib/open-system-browser.mjs create mode 100644 .opencode/skills/impeccable/scripts/lib/open-system-browser.mjs create mode 100644 .pi/skills/impeccable/scripts/lib/open-system-browser.mjs create mode 100644 .qoder/skills/impeccable/scripts/lib/open-system-browser.mjs create mode 100644 .rovodev/skills/impeccable/scripts/lib/open-system-browser.mjs create mode 100644 .trae-cn/skills/impeccable/scripts/lib/open-system-browser.mjs create mode 100644 .trae/skills/impeccable/scripts/lib/open-system-browser.mjs create mode 100644 .vibe/skills/impeccable/scripts/lib/open-system-browser.mjs create mode 100644 plugin/skills/impeccable/scripts/lib/open-system-browser.mjs 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/serve-question.mjs b/.agents/skills/impeccable/scripts/serve-question.mjs index 25beaa759..2d8c24a6a 100644 --- a/.agents/skills/impeccable/scripts/serve-question.mjs +++ b/.agents/skills/impeccable/scripts/serve-question.mjs @@ -79,6 +79,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; +import { openSystemBrowser } from './lib/open-system-browser.mjs'; function arg(name, fallback = null) { const i = process.argv.indexOf(`--${name}`); @@ -976,8 +977,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/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/serve-question.mjs b/.claude/skills/impeccable/scripts/serve-question.mjs index 25beaa759..2d8c24a6a 100644 --- a/.claude/skills/impeccable/scripts/serve-question.mjs +++ b/.claude/skills/impeccable/scripts/serve-question.mjs @@ -79,6 +79,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; +import { openSystemBrowser } from './lib/open-system-browser.mjs'; function arg(name, fallback = null) { const i = process.argv.indexOf(`--${name}`); @@ -976,8 +977,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/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/serve-question.mjs b/.cursor/skills/impeccable/scripts/serve-question.mjs index 25beaa759..2d8c24a6a 100644 --- a/.cursor/skills/impeccable/scripts/serve-question.mjs +++ b/.cursor/skills/impeccable/scripts/serve-question.mjs @@ -79,6 +79,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; +import { openSystemBrowser } from './lib/open-system-browser.mjs'; function arg(name, fallback = null) { const i = process.argv.indexOf(`--${name}`); @@ -976,8 +977,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/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/serve-question.mjs b/.gemini/skills/impeccable/scripts/serve-question.mjs index 25beaa759..2d8c24a6a 100644 --- a/.gemini/skills/impeccable/scripts/serve-question.mjs +++ b/.gemini/skills/impeccable/scripts/serve-question.mjs @@ -79,6 +79,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; +import { openSystemBrowser } from './lib/open-system-browser.mjs'; function arg(name, fallback = null) { const i = process.argv.indexOf(`--${name}`); @@ -976,8 +977,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/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/serve-question.mjs b/.github/skills/impeccable/scripts/serve-question.mjs index 25beaa759..2d8c24a6a 100644 --- a/.github/skills/impeccable/scripts/serve-question.mjs +++ b/.github/skills/impeccable/scripts/serve-question.mjs @@ -79,6 +79,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; +import { openSystemBrowser } from './lib/open-system-browser.mjs'; function arg(name, fallback = null) { const i = process.argv.indexOf(`--${name}`); @@ -976,8 +977,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/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/serve-question.mjs b/.grok/skills/impeccable/scripts/serve-question.mjs index 25beaa759..2d8c24a6a 100644 --- a/.grok/skills/impeccable/scripts/serve-question.mjs +++ b/.grok/skills/impeccable/scripts/serve-question.mjs @@ -79,6 +79,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; +import { openSystemBrowser } from './lib/open-system-browser.mjs'; function arg(name, fallback = null) { const i = process.argv.indexOf(`--${name}`); @@ -976,8 +977,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/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/serve-question.mjs b/.kiro/skills/impeccable/scripts/serve-question.mjs index 25beaa759..2d8c24a6a 100644 --- a/.kiro/skills/impeccable/scripts/serve-question.mjs +++ b/.kiro/skills/impeccable/scripts/serve-question.mjs @@ -79,6 +79,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; +import { openSystemBrowser } from './lib/open-system-browser.mjs'; function arg(name, fallback = null) { const i = process.argv.indexOf(`--${name}`); @@ -976,8 +977,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/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/serve-question.mjs b/.opencode/skills/impeccable/scripts/serve-question.mjs index 25beaa759..2d8c24a6a 100644 --- a/.opencode/skills/impeccable/scripts/serve-question.mjs +++ b/.opencode/skills/impeccable/scripts/serve-question.mjs @@ -79,6 +79,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; +import { openSystemBrowser } from './lib/open-system-browser.mjs'; function arg(name, fallback = null) { const i = process.argv.indexOf(`--${name}`); @@ -976,8 +977,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/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/serve-question.mjs b/.pi/skills/impeccable/scripts/serve-question.mjs index 25beaa759..2d8c24a6a 100644 --- a/.pi/skills/impeccable/scripts/serve-question.mjs +++ b/.pi/skills/impeccable/scripts/serve-question.mjs @@ -79,6 +79,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; +import { openSystemBrowser } from './lib/open-system-browser.mjs'; function arg(name, fallback = null) { const i = process.argv.indexOf(`--${name}`); @@ -976,8 +977,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/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/serve-question.mjs b/.qoder/skills/impeccable/scripts/serve-question.mjs index 25beaa759..2d8c24a6a 100644 --- a/.qoder/skills/impeccable/scripts/serve-question.mjs +++ b/.qoder/skills/impeccable/scripts/serve-question.mjs @@ -79,6 +79,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; +import { openSystemBrowser } from './lib/open-system-browser.mjs'; function arg(name, fallback = null) { const i = process.argv.indexOf(`--${name}`); @@ -976,8 +977,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/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/serve-question.mjs b/.rovodev/skills/impeccable/scripts/serve-question.mjs index 25beaa759..2d8c24a6a 100644 --- a/.rovodev/skills/impeccable/scripts/serve-question.mjs +++ b/.rovodev/skills/impeccable/scripts/serve-question.mjs @@ -79,6 +79,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; +import { openSystemBrowser } from './lib/open-system-browser.mjs'; function arg(name, fallback = null) { const i = process.argv.indexOf(`--${name}`); @@ -976,8 +977,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/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/serve-question.mjs b/.trae-cn/skills/impeccable/scripts/serve-question.mjs index 25beaa759..2d8c24a6a 100644 --- a/.trae-cn/skills/impeccable/scripts/serve-question.mjs +++ b/.trae-cn/skills/impeccable/scripts/serve-question.mjs @@ -79,6 +79,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; +import { openSystemBrowser } from './lib/open-system-browser.mjs'; function arg(name, fallback = null) { const i = process.argv.indexOf(`--${name}`); @@ -976,8 +977,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/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/serve-question.mjs b/.trae/skills/impeccable/scripts/serve-question.mjs index 25beaa759..2d8c24a6a 100644 --- a/.trae/skills/impeccable/scripts/serve-question.mjs +++ b/.trae/skills/impeccable/scripts/serve-question.mjs @@ -79,6 +79,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; +import { openSystemBrowser } from './lib/open-system-browser.mjs'; function arg(name, fallback = null) { const i = process.argv.indexOf(`--${name}`); @@ -976,8 +977,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/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/serve-question.mjs b/.vibe/skills/impeccable/scripts/serve-question.mjs index 25beaa759..2d8c24a6a 100644 --- a/.vibe/skills/impeccable/scripts/serve-question.mjs +++ b/.vibe/skills/impeccable/scripts/serve-question.mjs @@ -79,6 +79,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; +import { openSystemBrowser } from './lib/open-system-browser.mjs'; function arg(name, fallback = null) { const i = process.argv.indexOf(`--${name}`); @@ -976,8 +977,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/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/serve-question.mjs b/plugin/skills/impeccable/scripts/serve-question.mjs index 25beaa759..2d8c24a6a 100644 --- a/plugin/skills/impeccable/scripts/serve-question.mjs +++ b/plugin/skills/impeccable/scripts/serve-question.mjs @@ -79,6 +79,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; +import { openSystemBrowser } from './lib/open-system-browser.mjs'; function arg(name, fallback = null) { const i = process.argv.indexOf(`--${name}`); @@ -976,8 +977,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(() => { From 6886ab8c0eb9d98a4567dcef6b1f754475842abf Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Wed, 5 Aug 2026 15:25:42 -0700 Subject: [PATCH 11/35] Fix Codex pinned skill frontmatter (#519) Emit Codex-compatible top-level keys while preserving the argument hint under metadata. Keep existing Claude-style pin frontmatter unchanged for other harnesses. AI assistance: Codex implemented and validated this change under maintainer pbakaus's standing authorization. --- skill/scripts/pin.mjs | 10 ++++++---- tests/pin.test.mjs | 5 +++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/skill/scripts/pin.mjs b/skill/scripts/pin.mjs index 23466273f..27eb8be35 100644 --- a/skill/scripts/pin.mjs +++ b/skill/scripts/pin.mjs @@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) { return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; } -function generatePinnedSkill(command, metadata, commandPrefix) { +function generatePinnedSkill(command, metadata, commandPrefix, isCodex) { const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; + const providerFrontmatter = isCodex + ? `metadata:\n argument-hint: "${hint}"` + : `argument-hint: "${hint}"\nuser-invocable: true`; return `--- name: ${command} description: "${desc}" -argument-hint: "${hint}" -user-invocable: true +${providerFrontmatter} --- ${PIN_MARKER} @@ -128,7 +130,7 @@ function pin(command, projectRoot) { for (const skillsDir of harnessDirs) { const commandPrefix = commandPrefixForSkillsDir(skillsDir); - const content = generatePinnedSkill(command, metadata, commandPrefix); + const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$'); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { diff --git a/tests/pin.test.mjs b/tests/pin.test.mjs index ae58a8dec..8c6012458 100644 --- a/tests/pin.test.mjs +++ b/tests/pin.test.mjs @@ -35,12 +35,17 @@ describe('pin command provider syntax', () => { const skill = fs.readFileSync(path.join(project, harness, 'skills', 'audit', 'SKILL.md'), 'utf8'); assert.match(skill, /\/impeccable audit/); assert.doesNotMatch(skill, /\$impeccable audit/); + assert.match(skill, /^argument-hint:/m); + assert.match(skill, /^user-invocable: true$/m); } for (const harness of ['.agents', '.codex']) { const skill = fs.readFileSync(path.join(project, harness, 'skills', 'audit', 'SKILL.md'), 'utf8'); assert.match(skill, /\$impeccable audit/); assert.doesNotMatch(skill, /\/impeccable audit/); + assert.doesNotMatch(skill, /^argument-hint:/m); + assert.doesNotMatch(skill, /^user-invocable:/m); + assert.match(skill, /^metadata:\n argument-hint:/m); } }); }); From b14df9818322b13411181aaa9b0a6d4f56679a82 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 22:26:15 +0000 Subject: [PATCH 12/35] Sync generated provider output --- .agents/skills/impeccable/scripts/pin.mjs | 10 ++++++---- .claude/skills/impeccable/scripts/pin.mjs | 10 ++++++---- .cursor/skills/impeccable/scripts/pin.mjs | 10 ++++++---- .gemini/skills/impeccable/scripts/pin.mjs | 10 ++++++---- .github/skills/impeccable/scripts/pin.mjs | 10 ++++++---- .grok/skills/impeccable/scripts/pin.mjs | 10 ++++++---- .kiro/skills/impeccable/scripts/pin.mjs | 10 ++++++---- .opencode/skills/impeccable/scripts/pin.mjs | 10 ++++++---- .pi/skills/impeccable/scripts/pin.mjs | 10 ++++++---- .qoder/skills/impeccable/scripts/pin.mjs | 10 ++++++---- .rovodev/skills/impeccable/scripts/pin.mjs | 10 ++++++---- .trae-cn/skills/impeccable/scripts/pin.mjs | 10 ++++++---- .trae/skills/impeccable/scripts/pin.mjs | 10 ++++++---- .vibe/skills/impeccable/scripts/pin.mjs | 10 ++++++---- plugin/skills/impeccable/scripts/pin.mjs | 10 ++++++---- 15 files changed, 90 insertions(+), 60 deletions(-) diff --git a/.agents/skills/impeccable/scripts/pin.mjs b/.agents/skills/impeccable/scripts/pin.mjs index 23466273f..27eb8be35 100644 --- a/.agents/skills/impeccable/scripts/pin.mjs +++ b/.agents/skills/impeccable/scripts/pin.mjs @@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) { return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; } -function generatePinnedSkill(command, metadata, commandPrefix) { +function generatePinnedSkill(command, metadata, commandPrefix, isCodex) { const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; + const providerFrontmatter = isCodex + ? `metadata:\n argument-hint: "${hint}"` + : `argument-hint: "${hint}"\nuser-invocable: true`; return `--- name: ${command} description: "${desc}" -argument-hint: "${hint}" -user-invocable: true +${providerFrontmatter} --- ${PIN_MARKER} @@ -128,7 +130,7 @@ function pin(command, projectRoot) { for (const skillsDir of harnessDirs) { const commandPrefix = commandPrefixForSkillsDir(skillsDir); - const content = generatePinnedSkill(command, metadata, commandPrefix); + const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$'); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { diff --git a/.claude/skills/impeccable/scripts/pin.mjs b/.claude/skills/impeccable/scripts/pin.mjs index 23466273f..27eb8be35 100644 --- a/.claude/skills/impeccable/scripts/pin.mjs +++ b/.claude/skills/impeccable/scripts/pin.mjs @@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) { return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; } -function generatePinnedSkill(command, metadata, commandPrefix) { +function generatePinnedSkill(command, metadata, commandPrefix, isCodex) { const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; + const providerFrontmatter = isCodex + ? `metadata:\n argument-hint: "${hint}"` + : `argument-hint: "${hint}"\nuser-invocable: true`; return `--- name: ${command} description: "${desc}" -argument-hint: "${hint}" -user-invocable: true +${providerFrontmatter} --- ${PIN_MARKER} @@ -128,7 +130,7 @@ function pin(command, projectRoot) { for (const skillsDir of harnessDirs) { const commandPrefix = commandPrefixForSkillsDir(skillsDir); - const content = generatePinnedSkill(command, metadata, commandPrefix); + const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$'); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { diff --git a/.cursor/skills/impeccable/scripts/pin.mjs b/.cursor/skills/impeccable/scripts/pin.mjs index 23466273f..27eb8be35 100644 --- a/.cursor/skills/impeccable/scripts/pin.mjs +++ b/.cursor/skills/impeccable/scripts/pin.mjs @@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) { return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; } -function generatePinnedSkill(command, metadata, commandPrefix) { +function generatePinnedSkill(command, metadata, commandPrefix, isCodex) { const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; + const providerFrontmatter = isCodex + ? `metadata:\n argument-hint: "${hint}"` + : `argument-hint: "${hint}"\nuser-invocable: true`; return `--- name: ${command} description: "${desc}" -argument-hint: "${hint}" -user-invocable: true +${providerFrontmatter} --- ${PIN_MARKER} @@ -128,7 +130,7 @@ function pin(command, projectRoot) { for (const skillsDir of harnessDirs) { const commandPrefix = commandPrefixForSkillsDir(skillsDir); - const content = generatePinnedSkill(command, metadata, commandPrefix); + const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$'); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { diff --git a/.gemini/skills/impeccable/scripts/pin.mjs b/.gemini/skills/impeccable/scripts/pin.mjs index 23466273f..27eb8be35 100644 --- a/.gemini/skills/impeccable/scripts/pin.mjs +++ b/.gemini/skills/impeccable/scripts/pin.mjs @@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) { return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; } -function generatePinnedSkill(command, metadata, commandPrefix) { +function generatePinnedSkill(command, metadata, commandPrefix, isCodex) { const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; + const providerFrontmatter = isCodex + ? `metadata:\n argument-hint: "${hint}"` + : `argument-hint: "${hint}"\nuser-invocable: true`; return `--- name: ${command} description: "${desc}" -argument-hint: "${hint}" -user-invocable: true +${providerFrontmatter} --- ${PIN_MARKER} @@ -128,7 +130,7 @@ function pin(command, projectRoot) { for (const skillsDir of harnessDirs) { const commandPrefix = commandPrefixForSkillsDir(skillsDir); - const content = generatePinnedSkill(command, metadata, commandPrefix); + const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$'); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { diff --git a/.github/skills/impeccable/scripts/pin.mjs b/.github/skills/impeccable/scripts/pin.mjs index 23466273f..27eb8be35 100644 --- a/.github/skills/impeccable/scripts/pin.mjs +++ b/.github/skills/impeccable/scripts/pin.mjs @@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) { return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; } -function generatePinnedSkill(command, metadata, commandPrefix) { +function generatePinnedSkill(command, metadata, commandPrefix, isCodex) { const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; + const providerFrontmatter = isCodex + ? `metadata:\n argument-hint: "${hint}"` + : `argument-hint: "${hint}"\nuser-invocable: true`; return `--- name: ${command} description: "${desc}" -argument-hint: "${hint}" -user-invocable: true +${providerFrontmatter} --- ${PIN_MARKER} @@ -128,7 +130,7 @@ function pin(command, projectRoot) { for (const skillsDir of harnessDirs) { const commandPrefix = commandPrefixForSkillsDir(skillsDir); - const content = generatePinnedSkill(command, metadata, commandPrefix); + const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$'); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { diff --git a/.grok/skills/impeccable/scripts/pin.mjs b/.grok/skills/impeccable/scripts/pin.mjs index 23466273f..27eb8be35 100644 --- a/.grok/skills/impeccable/scripts/pin.mjs +++ b/.grok/skills/impeccable/scripts/pin.mjs @@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) { return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; } -function generatePinnedSkill(command, metadata, commandPrefix) { +function generatePinnedSkill(command, metadata, commandPrefix, isCodex) { const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; + const providerFrontmatter = isCodex + ? `metadata:\n argument-hint: "${hint}"` + : `argument-hint: "${hint}"\nuser-invocable: true`; return `--- name: ${command} description: "${desc}" -argument-hint: "${hint}" -user-invocable: true +${providerFrontmatter} --- ${PIN_MARKER} @@ -128,7 +130,7 @@ function pin(command, projectRoot) { for (const skillsDir of harnessDirs) { const commandPrefix = commandPrefixForSkillsDir(skillsDir); - const content = generatePinnedSkill(command, metadata, commandPrefix); + const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$'); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { diff --git a/.kiro/skills/impeccable/scripts/pin.mjs b/.kiro/skills/impeccable/scripts/pin.mjs index 23466273f..27eb8be35 100644 --- a/.kiro/skills/impeccable/scripts/pin.mjs +++ b/.kiro/skills/impeccable/scripts/pin.mjs @@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) { return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; } -function generatePinnedSkill(command, metadata, commandPrefix) { +function generatePinnedSkill(command, metadata, commandPrefix, isCodex) { const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; + const providerFrontmatter = isCodex + ? `metadata:\n argument-hint: "${hint}"` + : `argument-hint: "${hint}"\nuser-invocable: true`; return `--- name: ${command} description: "${desc}" -argument-hint: "${hint}" -user-invocable: true +${providerFrontmatter} --- ${PIN_MARKER} @@ -128,7 +130,7 @@ function pin(command, projectRoot) { for (const skillsDir of harnessDirs) { const commandPrefix = commandPrefixForSkillsDir(skillsDir); - const content = generatePinnedSkill(command, metadata, commandPrefix); + const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$'); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { diff --git a/.opencode/skills/impeccable/scripts/pin.mjs b/.opencode/skills/impeccable/scripts/pin.mjs index 23466273f..27eb8be35 100644 --- a/.opencode/skills/impeccable/scripts/pin.mjs +++ b/.opencode/skills/impeccable/scripts/pin.mjs @@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) { return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; } -function generatePinnedSkill(command, metadata, commandPrefix) { +function generatePinnedSkill(command, metadata, commandPrefix, isCodex) { const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; + const providerFrontmatter = isCodex + ? `metadata:\n argument-hint: "${hint}"` + : `argument-hint: "${hint}"\nuser-invocable: true`; return `--- name: ${command} description: "${desc}" -argument-hint: "${hint}" -user-invocable: true +${providerFrontmatter} --- ${PIN_MARKER} @@ -128,7 +130,7 @@ function pin(command, projectRoot) { for (const skillsDir of harnessDirs) { const commandPrefix = commandPrefixForSkillsDir(skillsDir); - const content = generatePinnedSkill(command, metadata, commandPrefix); + const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$'); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { diff --git a/.pi/skills/impeccable/scripts/pin.mjs b/.pi/skills/impeccable/scripts/pin.mjs index 23466273f..27eb8be35 100644 --- a/.pi/skills/impeccable/scripts/pin.mjs +++ b/.pi/skills/impeccable/scripts/pin.mjs @@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) { return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; } -function generatePinnedSkill(command, metadata, commandPrefix) { +function generatePinnedSkill(command, metadata, commandPrefix, isCodex) { const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; + const providerFrontmatter = isCodex + ? `metadata:\n argument-hint: "${hint}"` + : `argument-hint: "${hint}"\nuser-invocable: true`; return `--- name: ${command} description: "${desc}" -argument-hint: "${hint}" -user-invocable: true +${providerFrontmatter} --- ${PIN_MARKER} @@ -128,7 +130,7 @@ function pin(command, projectRoot) { for (const skillsDir of harnessDirs) { const commandPrefix = commandPrefixForSkillsDir(skillsDir); - const content = generatePinnedSkill(command, metadata, commandPrefix); + const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$'); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { diff --git a/.qoder/skills/impeccable/scripts/pin.mjs b/.qoder/skills/impeccable/scripts/pin.mjs index 23466273f..27eb8be35 100644 --- a/.qoder/skills/impeccable/scripts/pin.mjs +++ b/.qoder/skills/impeccable/scripts/pin.mjs @@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) { return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; } -function generatePinnedSkill(command, metadata, commandPrefix) { +function generatePinnedSkill(command, metadata, commandPrefix, isCodex) { const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; + const providerFrontmatter = isCodex + ? `metadata:\n argument-hint: "${hint}"` + : `argument-hint: "${hint}"\nuser-invocable: true`; return `--- name: ${command} description: "${desc}" -argument-hint: "${hint}" -user-invocable: true +${providerFrontmatter} --- ${PIN_MARKER} @@ -128,7 +130,7 @@ function pin(command, projectRoot) { for (const skillsDir of harnessDirs) { const commandPrefix = commandPrefixForSkillsDir(skillsDir); - const content = generatePinnedSkill(command, metadata, commandPrefix); + const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$'); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { diff --git a/.rovodev/skills/impeccable/scripts/pin.mjs b/.rovodev/skills/impeccable/scripts/pin.mjs index 23466273f..27eb8be35 100644 --- a/.rovodev/skills/impeccable/scripts/pin.mjs +++ b/.rovodev/skills/impeccable/scripts/pin.mjs @@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) { return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; } -function generatePinnedSkill(command, metadata, commandPrefix) { +function generatePinnedSkill(command, metadata, commandPrefix, isCodex) { const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; + const providerFrontmatter = isCodex + ? `metadata:\n argument-hint: "${hint}"` + : `argument-hint: "${hint}"\nuser-invocable: true`; return `--- name: ${command} description: "${desc}" -argument-hint: "${hint}" -user-invocable: true +${providerFrontmatter} --- ${PIN_MARKER} @@ -128,7 +130,7 @@ function pin(command, projectRoot) { for (const skillsDir of harnessDirs) { const commandPrefix = commandPrefixForSkillsDir(skillsDir); - const content = generatePinnedSkill(command, metadata, commandPrefix); + const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$'); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { diff --git a/.trae-cn/skills/impeccable/scripts/pin.mjs b/.trae-cn/skills/impeccable/scripts/pin.mjs index 23466273f..27eb8be35 100644 --- a/.trae-cn/skills/impeccable/scripts/pin.mjs +++ b/.trae-cn/skills/impeccable/scripts/pin.mjs @@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) { return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; } -function generatePinnedSkill(command, metadata, commandPrefix) { +function generatePinnedSkill(command, metadata, commandPrefix, isCodex) { const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; + const providerFrontmatter = isCodex + ? `metadata:\n argument-hint: "${hint}"` + : `argument-hint: "${hint}"\nuser-invocable: true`; return `--- name: ${command} description: "${desc}" -argument-hint: "${hint}" -user-invocable: true +${providerFrontmatter} --- ${PIN_MARKER} @@ -128,7 +130,7 @@ function pin(command, projectRoot) { for (const skillsDir of harnessDirs) { const commandPrefix = commandPrefixForSkillsDir(skillsDir); - const content = generatePinnedSkill(command, metadata, commandPrefix); + const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$'); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { diff --git a/.trae/skills/impeccable/scripts/pin.mjs b/.trae/skills/impeccable/scripts/pin.mjs index 23466273f..27eb8be35 100644 --- a/.trae/skills/impeccable/scripts/pin.mjs +++ b/.trae/skills/impeccable/scripts/pin.mjs @@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) { return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; } -function generatePinnedSkill(command, metadata, commandPrefix) { +function generatePinnedSkill(command, metadata, commandPrefix, isCodex) { const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; + const providerFrontmatter = isCodex + ? `metadata:\n argument-hint: "${hint}"` + : `argument-hint: "${hint}"\nuser-invocable: true`; return `--- name: ${command} description: "${desc}" -argument-hint: "${hint}" -user-invocable: true +${providerFrontmatter} --- ${PIN_MARKER} @@ -128,7 +130,7 @@ function pin(command, projectRoot) { for (const skillsDir of harnessDirs) { const commandPrefix = commandPrefixForSkillsDir(skillsDir); - const content = generatePinnedSkill(command, metadata, commandPrefix); + const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$'); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { diff --git a/.vibe/skills/impeccable/scripts/pin.mjs b/.vibe/skills/impeccable/scripts/pin.mjs index 23466273f..27eb8be35 100644 --- a/.vibe/skills/impeccable/scripts/pin.mjs +++ b/.vibe/skills/impeccable/scripts/pin.mjs @@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) { return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; } -function generatePinnedSkill(command, metadata, commandPrefix) { +function generatePinnedSkill(command, metadata, commandPrefix, isCodex) { const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; + const providerFrontmatter = isCodex + ? `metadata:\n argument-hint: "${hint}"` + : `argument-hint: "${hint}"\nuser-invocable: true`; return `--- name: ${command} description: "${desc}" -argument-hint: "${hint}" -user-invocable: true +${providerFrontmatter} --- ${PIN_MARKER} @@ -128,7 +130,7 @@ function pin(command, projectRoot) { for (const skillsDir of harnessDirs) { const commandPrefix = commandPrefixForSkillsDir(skillsDir); - const content = generatePinnedSkill(command, metadata, commandPrefix); + const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$'); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { diff --git a/plugin/skills/impeccable/scripts/pin.mjs b/plugin/skills/impeccable/scripts/pin.mjs index 23466273f..27eb8be35 100644 --- a/plugin/skills/impeccable/scripts/pin.mjs +++ b/plugin/skills/impeccable/scripts/pin.mjs @@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) { return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; } -function generatePinnedSkill(command, metadata, commandPrefix) { +function generatePinnedSkill(command, metadata, commandPrefix, isCodex) { const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; + const providerFrontmatter = isCodex + ? `metadata:\n argument-hint: "${hint}"` + : `argument-hint: "${hint}"\nuser-invocable: true`; return `--- name: ${command} description: "${desc}" -argument-hint: "${hint}" -user-invocable: true +${providerFrontmatter} --- ${PIN_MARKER} @@ -128,7 +130,7 @@ function pin(command, projectRoot) { for (const skillsDir of harnessDirs) { const commandPrefix = commandPrefixForSkillsDir(skillsDir); - const content = generatePinnedSkill(command, metadata, commandPrefix); + const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$'); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { From e46e0da8852aba714d070ad5ec00264540e72d02 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Wed, 5 Aug 2026 15:27:02 -0700 Subject: [PATCH 13/35] Centralize critique snapshot reading (#511) Make critique storage the single owner of snapshot discovery and frontmatter parsing, and keep context signals focused on summarizing the canonical result. AI-assisted: Prepared by Codex under pbakaus's scheduled architecture-refactor authorization. --- skill/scripts/context-signals.mjs | 22 ++++++---------------- skill/scripts/critique-storage.mjs | 29 +++++++++++++++++++---------- tests/context-signals.test.mjs | 16 ++++++++++++++++ tests/critique-storage.test.mjs | 13 ++++++++++++- 4 files changed, 53 insertions(+), 27 deletions(-) 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/tests/context-signals.test.mjs b/tests/context-signals.test.mjs index 505258c76..5480496a3 100644 --- a/tests/context-signals.test.mjs +++ b/tests/context-signals.test.mjs @@ -76,6 +76,22 @@ describe('gatherSignals', () => { assert.equal(s.critique.latest.slug, 'home'); }); + it('reads the newest critique snapshot across target slugs', async () => { + write('.impeccable/critique/2026-05-01T10-00-00Z__home.md', + '---\nslug: home\nscore: 6\np0: 1\np1: 3\ntimestamp: 2026-05-01T10-00-00Z\n---\nbody\n'); + write('.impeccable/critique/2026-05-02T10-00-00Z__pricing.md', + '---\nslug: pricing\nscore: 9\np0: 0\np1: 1\ntimestamp: 2026-05-02T10-00-00Z\n---\nbody\n'); + write('.impeccable/critique/ignore.md', '# Critique ignores\n'); + write('.impeccable/critique/9999-not-a-snapshot.md', '# Draft\n'); + const s = await gatherSignals(scratch); + assert.equal(s.critique.latest.slug, 'pricing'); + assert.equal(s.critique.latest.score, 9); + assert.equal( + s.critique.latest.file, + '.impeccable/critique/2026-05-02T10-00-00Z__pricing.md', + ); + }); + it('handles a non-git dir without throwing', async () => { const s = await gatherSignals(scratch); assert.equal(s.git.isRepo, false); diff --git a/tests/critique-storage.test.mjs b/tests/critique-storage.test.mjs index 6fcf746e2..3f21601b2 100644 --- a/tests/critique-storage.test.mjs +++ b/tests/critique-storage.test.mjs @@ -5,7 +5,7 @@ import { describe, it, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert/strict'; -import { mkdtempSync, rmSync, symlinkSync } from 'node:fs'; +import { mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { spawnSync } from 'node:child_process'; @@ -17,6 +17,7 @@ import { slugFromTarget, writeSnapshot, readLatestSnapshot, + readLatestSnapshotAcrossTargets, readTrend, nowFilenameStamp, } from '../skill/scripts/critique-storage.mjs'; @@ -114,6 +115,16 @@ describe('writeSnapshot + readLatestSnapshot', () => { assert.match(latest.body, /new/); }); + it('picks the newest snapshot across target slugs', () => { + writeSnapshot({ slug: 'home', meta: {}, body: 'old', cwd, now: new Date('2026-05-01T00:00:00Z') }); + writeSnapshot({ slug: 'pricing', meta: {}, body: 'new', cwd, now: new Date('2026-05-12T00:00:00Z') }); + writeFileSync(join(cwd, '.impeccable', 'critique', 'ignore.md'), '# Critique ignores\n'); + writeFileSync(join(cwd, '.impeccable', 'critique', '9999-not-a-snapshot.md'), '# Draft\n'); + const latest = readLatestSnapshotAcrossTargets({ cwd }); + assert.equal(latest.meta.slug, 'pricing'); + assert.match(latest.body, /new/); + }); + it('does not see snapshots for a different slug', () => { writeSnapshot({ slug: 'pricing-astro', meta: { total_score: 10 }, body: 'b', cwd }); assert.equal(readLatestSnapshot('index-astro', { cwd }), null); From e76b3424d29db93e80ec893a6195f6d739c66cd9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 22:27:35 +0000 Subject: [PATCH 14/35] Sync generated provider output --- .../impeccable/scripts/context-signals.mjs | 22 ++++---------- .../impeccable/scripts/critique-storage.mjs | 29 ++++++++++++------- .../impeccable/scripts/context-signals.mjs | 22 ++++---------- .../impeccable/scripts/critique-storage.mjs | 29 ++++++++++++------- .../impeccable/scripts/context-signals.mjs | 22 ++++---------- .../impeccable/scripts/critique-storage.mjs | 29 ++++++++++++------- .../impeccable/scripts/context-signals.mjs | 22 ++++---------- .../impeccable/scripts/critique-storage.mjs | 29 ++++++++++++------- .../impeccable/scripts/context-signals.mjs | 22 ++++---------- .../impeccable/scripts/critique-storage.mjs | 29 ++++++++++++------- .../impeccable/scripts/context-signals.mjs | 22 ++++---------- .../impeccable/scripts/critique-storage.mjs | 29 ++++++++++++------- .../impeccable/scripts/context-signals.mjs | 22 ++++---------- .../impeccable/scripts/critique-storage.mjs | 29 ++++++++++++------- .../impeccable/scripts/context-signals.mjs | 22 ++++---------- .../impeccable/scripts/critique-storage.mjs | 29 ++++++++++++------- .../impeccable/scripts/context-signals.mjs | 22 ++++---------- .../impeccable/scripts/critique-storage.mjs | 29 ++++++++++++------- .../impeccable/scripts/context-signals.mjs | 22 ++++---------- .../impeccable/scripts/critique-storage.mjs | 29 ++++++++++++------- .../impeccable/scripts/context-signals.mjs | 22 ++++---------- .../impeccable/scripts/critique-storage.mjs | 29 ++++++++++++------- .../impeccable/scripts/context-signals.mjs | 22 ++++---------- .../impeccable/scripts/critique-storage.mjs | 29 ++++++++++++------- .../impeccable/scripts/context-signals.mjs | 22 ++++---------- .../impeccable/scripts/critique-storage.mjs | 29 ++++++++++++------- .../impeccable/scripts/context-signals.mjs | 22 ++++---------- .../impeccable/scripts/critique-storage.mjs | 29 ++++++++++++------- .../impeccable/scripts/context-signals.mjs | 22 ++++---------- .../impeccable/scripts/critique-storage.mjs | 29 ++++++++++++------- 30 files changed, 375 insertions(+), 390 deletions(-) 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/.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/.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/.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/.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/.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/.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/.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/.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/.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/.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/.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/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/.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/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'))); } From a075d89bdbe60b2b00220cb0527fb5091e84215e Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Wed, 5 Aug 2026 15:28:17 -0700 Subject: [PATCH 15/35] Simplify CSS color channel parsing (#520) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Centralize CSS numeric token parsing and characterize every supported color unit while preserving config and filtering behavior. AI-assisted: Codex implemented this refactor under pbakaus’s scheduled architecture-simplification authorization. --- cli/lib/impeccable-config.mjs | 98 ++++++++++++----------------- tests/lib/impeccable-config.test.js | 47 ++++++++++++++ 2 files changed, 87 insertions(+), 58 deletions(-) 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/tests/lib/impeccable-config.test.js b/tests/lib/impeccable-config.test.js index 039fef2a3..8ca7aeaa3 100644 --- a/tests/lib/impeccable-config.test.js +++ b/tests/lib/impeccable-config.test.js @@ -235,6 +235,53 @@ describe('cli/lib/impeccable-config', () => { ]); }); + test('filterDetectionFindings normalizes every supported CSS color unit', () => { + const findings = [ + { antipattern: 'design-system-color', line: 1, ignoreValue: '#f00' }, + { antipattern: 'design-system-color', line: 2, ignoreValue: 'rgb(100% 0% 0%)' }, + { antipattern: 'design-system-color', line: 3, ignoreValue: 'hsl(360deg 100% 50%)' }, + { antipattern: 'design-system-color', line: 4, ignoreValue: 'hsl(180deg 100% 50%)' }, + { antipattern: 'design-system-color', line: 5, ignoreValue: 'hsl(3.141592653589793rad 100% 50%)' }, + { antipattern: 'design-system-color', line: 6, ignoreValue: 'hsl(0.5turn 100% 50%)' }, + { antipattern: 'design-system-color', line: 7, ignoreValue: 'hsl(200grad 100% 50%)' }, + { antipattern: 'design-system-color', line: 8, ignoreValue: 'rgba(255, 0, 0, 0.5)' }, + { antipattern: 'design-system-color', line: 9, ignoreValue: 'rgb(100% 0% 0% / 50%)' }, + { antipattern: 'design-system-color', line: 10, ignoreValue: 'hsla(0, 100%, 50%, 50%)' }, + { antipattern: 'design-system-color', line: 11, ignoreValue: '#f008' }, + ]; + const filtered = filterDetectionFindings(findings, { + ignoreValues: [ + { rule: 'design-system-color', value: '#ff0000' }, + { rule: 'design-system-color', value: '#00ffff' }, + { rule: 'design-system-color', value: '#ff000080' }, + { rule: 'design-system-color', value: '#ff000088' }, + ], + }); + + expect(filtered).toEqual([]); + }); + + test('filterDetectionFindings rejects out-of-range and malformed CSS colors', () => { + const findings = [ + { antipattern: 'design-system-color', line: 1, ignoreValue: 'rgb(256 0 0)' }, + { antipattern: 'design-system-color', line: 2, ignoreValue: 'rgb(100.1% 0% 0%)' }, + { antipattern: 'design-system-color', line: 3, ignoreValue: 'rgba(255, 0, 0, 101%)' }, + { antipattern: 'design-system-color', line: 4, ignoreValue: 'rgba(255, 0, 0, -0.1)' }, + { antipattern: 'design-system-color', line: 5, ignoreValue: 'hsl(0 100 50%)' }, + { antipattern: 'design-system-color', line: 6, ignoreValue: 'hsl(0 101% 50%)' }, + { antipattern: 'design-system-color', line: 7, ignoreValue: 'hsl(0foo 100% 50%)' }, + { antipattern: 'design-system-color', line: 8, ignoreValue: '#ff00000' }, + ]; + const filtered = filterDetectionFindings(findings, { + ignoreValues: [ + { rule: 'design-system-color', value: '#ff0000' }, + { rule: 'design-system-color', value: '#ff000080' }, + ], + }); + + expect(filtered.map((finding) => finding.line)).toEqual([1, 2, 3, 4, 5, 6, 7, 8]); + }); + test('extractFindingIgnoreValue handles fonts, Google font URLs, and motion snippets', () => { expect(extractFindingIgnoreValue({ antipattern: 'overused-font', snippet: 'Primary font: Avenir Next (80% of text)' })).toBe('avenir next'); expect(extractFindingIgnoreValue({ antipattern: 'overused-font', snippet: 'https://fonts.googleapis.com/css2?family=Alumni+Sans:wght@700' })).toBe('alumni sans'); From aee6ce9352b842217b3f57c78296a7a4fa35a7f3 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Tue, 4 Aug 2026 14:12:19 -0700 Subject: [PATCH 16/35] Give the Live UI surface inventory one definition again The list of Live chrome surfaces was inlined into live-browser.js as a function-scope const when live/ui-core.mjs was deleted for having zero in-repo references. It had one out-of-repo reference. The private impeccable-site repo imports it at build time: its Live UI lab must hold a snapshot for every surface Live defines, and the site build fails with the surface name when one is missing. Inlining put the list out of reach of every Node importer, so the site had to regex it back out of the browser script, and the guard only kept passing because the site's materialized copy of skill/ was stale. A guard that reads a list the site itself maintains guards nothing, so the fix is a real export rather than a better parser. skill/scripts/live/ui-surfaces.mjs is now the single definition. The browser-runtime constraint is unchanged and satisfied the same way the command palette already solves it: live-browser.js is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/tests/concept-seed.test.mjs b/tests/concept-seed.test.mjs index 2717dd849..e1963f84c 100644 --- a/tests/concept-seed.test.mjs +++ b/tests/concept-seed.test.mjs @@ -11,7 +11,7 @@ import { validateConceptEntry, } from '../skill/scripts/lib/concept-catalog.mjs'; import { readCompositionCatalog } from '../skill/scripts/lib/composition-catalog.mjs'; -import { dealCompositions, renderChallenger, selectApprovedChallengers, selectApprovedComposition, selectApprovedCompositions } from '../skill/scripts/concept-seed.mjs'; +import { dealCompositions, pingChosen, renderChallenger, selectApprovedChallengers, selectApprovedComposition, selectApprovedCompositions } from '../skill/scripts/concept-seed.mjs'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const SCRIPT = path.join(ROOT, 'skill', 'scripts', 'concept-seed.mjs'); @@ -251,6 +251,54 @@ describe('concept seed scopes', () => { assert.match(invalid.stderr, /non-negative integer/); }); + it('registers steer the round presentation without changing the deal', () => { + const plain = run('direction', ['--reroll', '1']); + const bolder = run('direction', ['--reroll', '1', '--register', 'bolder']); + const safer = run('direction', ['--reroll', '1', '--register', 'safer']); + assert.equal(bolder.status, 0); + assert.equal(safer.status, 0); + // A register is presentation-only: the same key and reroll count deal the + // same challengers, so the exclusion chain never forks on register. + const dealtIds = (out) => [...out.matchAll(/SOURCE ID: ([a-z0-9-]+)/g)].map((m) => m[1]).sort(); + assert.deepEqual(dealtIds(bolder.stdout), dealtIds(plain.stdout), 'bolder presents the same deal the plain round drew'); + assert.match(bolder.stdout, /BOLDER REGISTER/); + assert.match(bolder.stdout, /FIRST dealt challenger leads/); + assert.match(bolder.stdout, /--register bolder/); + assert.doesNotMatch(bolder.stdout, /ASSIGNED INDEX:/); + // The generic weighing instruction measures against the assigned + // direction, which a bolder round suspended; bolder weighs against the + // leader instead, and the contradiction must not ship. + assert.match(bolder.stdout, /against the fused LEADER/); + assert.doesNotMatch(bolder.stdout, /against the assigned direction/); + assert.match(safer.stdout, /SAFER REGISTER/); + assert.match(safer.stdout, /sanctioned lineup/); + assert.doesNotMatch(safer.stdout, /^CHALLENGERS:/m, 'the safer round spends its hand unseen'); + // Degraded safer must not contradict itself: "the user picks" and a + // mandatory numbered build order cannot share one output. + const degradedSafer = run('direction', ['--reroll', '1', '--register', 'safer'], { + IMPECCABLE_CATALOG_DIR: '/nonexistent-catalog-dir', + IMPECCABLE_API_URL: 'http://127.0.0.1:1', + }); + assert.equal(degradedSafer.status, 0); + assert.match(degradedSafer.stdout, /source: degraded/); + assert.match(degradedSafer.stdout, /SAFER REGISTER/); + assert.doesNotMatch(degradedSafer.stdout, /ASSIGNED INDEX/, 'degraded safer suppresses the assignment machinery'); + assert.doesNotMatch(degradedSafer.stdout, /Build candidate/, 'degraded safer mandates no numbered candidate'); + const degradedBolder = run('direction', ['--reroll', '1', '--register', 'bolder'], { + IMPECCABLE_CATALOG_DIR: '/nonexistent-catalog-dir', + IMPECCABLE_API_URL: 'http://127.0.0.1:1', + }); + assert.equal(degradedBolder.status, 0); + assert.match(degradedBolder.stdout, /BOLDER REGISTER UNAVAILABLE/); + assert.match(degradedBolder.stdout, /ASSIGNED INDEX: /, 'degraded bolder falls back to the plain grounded assignment'); + const invalidRegister = run('direction', ['--reroll', '1', '--register', 'wilder']); + assert.notEqual(invalidRegister.status, 0); + assert.match(invalidRegister.stderr, /must be safer or bolder/); + const noReroll = run('direction', ['--register', 'bolder']); + assert.notEqual(noReroll.status, 0); + assert.match(noReroll.stderr, /re-roll round/); + }); + it('filters challengers by strength per scope and falls back when a tier has no match', () => { const make = (id, tier, strength) => ({ id, @@ -481,6 +529,50 @@ describe('init gate', () => { }); assert.equal(result.status, 0); assert.doesNotMatch(result.stdout, /NO_PRODUCT_MD/); + // --kind alone is a valid ping invocation (assigned/pick/canon outcomes + // have no catalog id) and is equally ungated. + const kindOnly = spawnSync(process.execPath, [SCRIPT, '--kind', 'assigned', '--from', 'gate-test'], { + cwd: dir, + encoding: 'utf-8', + env: { ...process.env, IMPECCABLE_CATALOG_DIR: FIXTURE_DIR, IMPECCABLE_NO_TELEMETRY: '1' }, + }); + assert.equal(kindOnly.status, 0); + assert.doesNotMatch(kindOnly.stdout, /NO_PRODUCT_MD/); + assert.match(kindOnly.stdout, /choice ping skipped/, 'telemetry-disabled kind ping reports skipped, not an error'); + }); + + it('pingChosen validates kinds, requires ids only for challenger wins, and honors opt-out', async () => { + const calls = []; + const realFetch = globalThis.fetch; + globalThis.fetch = async (url, opts) => { calls.push(JSON.parse(opts.body)); return { ok: true }; }; + // telemetryDisabled() honors DO_NOT_TRACK too, so a developer shell with + // it set must not fail the success-path assertions below. + const savedDnt = process.env.DO_NOT_TRACK; + const savedNoTelemetry = process.env.IMPECCABLE_NO_TELEMETRY; + try { + delete process.env.DO_NOT_TRACK; + process.env.IMPECCABLE_NO_TELEMETRY = '1'; + assert.equal(await pingChosen({ kind: 'assigned', key: 'k' }), false, 'opt-out wins over everything'); + delete process.env.IMPECCABLE_NO_TELEMETRY; + assert.equal(await pingChosen({ kind: 'assigned', key: 'k', scope: 'direction' }), true, 'kind-only ping for a non-challenger outcome'); + assert.equal(await pingChosen({ kind: 'challenger', key: 'k' }), false, 'a challenger win without an id is not a ping'); + assert.equal(await pingChosen({ kind: 'weird', chosenId: 'x', key: 'k' }), false, 'unknown kinds are dropped'); + assert.equal(await pingChosen({ kind: 'assigned', register: 'wilder', key: 'k' }), false, 'unknown registers are dropped'); + assert.equal(await pingChosen({ chosenId: 'legacy-id', key: 'k' }), true, 'legacy id-only shape stays valid'); + assert.equal(await pingChosen({ kind: 'canon', register: 'safer', key: 'k' }), true, 'register rides along on a steered round'); + const bodies = calls; + assert.equal(bodies[0].kind, 'assigned'); + assert.equal(bodies[0].chosenId, undefined, 'no id field on kind-only pings'); + assert.equal(bodies[1].chosenId, 'legacy-id'); + assert.equal(bodies[1].kind, undefined, 'legacy pings carry no kind'); + assert.equal(bodies[2].register, 'safer'); + } finally { + globalThis.fetch = realFetch; + if (savedDnt === undefined) delete process.env.DO_NOT_TRACK; + else process.env.DO_NOT_TRACK = savedDnt; + if (savedNoTelemetry === undefined) delete process.env.IMPECCABLE_NO_TELEMETRY; + else process.env.IMPECCABLE_NO_TELEMETRY = savedNoTelemetry; + } }); // Mode eligibility on worlds. Before this, selectApprovedChallengers never diff --git a/tests/new-work-e2e.test.mjs b/tests/new-work-e2e.test.mjs index 327cf07d8..067c2c002 100644 --- a/tests/new-work-e2e.test.mjs +++ b/tests/new-work-e2e.test.mjs @@ -267,7 +267,7 @@ describe('new-work-e2e: serve-question decision page', () => { } }); - it('(e) an option with no hero renders a text-only card (no .media element)', async () => { + it('(e) a text-only assigned card caps every card at thumb imagery (salience parity)', async () => { const cwd = makeWorkspace(); const key = 'textonly'; const hero = makeFakeImage(cwd, 'has a hero', 'hero.png'); @@ -295,17 +295,173 @@ describe('new-work-e2e: serve-question decision page', () => { // on the front, where nothing else would have used the room. const textOnlyBack = await page.$('.card[data-id="assigned"] .face.back'); const frontRead = await page.$eval('.card[data-id="assigned"] .face.front', (el) => el.textContent); - // Catalog art without a sketch is labeled as reference, never as the - // promise of the build. - const heroLabel = await page.$eval('.card[data-id="challenger-hero"] .media .media-label', (el) => el.textContent); + // Salience parity: with a text-only assigned card, a challenger's + // catalog art may not render as a full-bleed face beside it; it demotes + // to a labeled thumb in the body, so pretty pixels never outvote the + // weighing. The thumb stays labeled as reference, never as the promise + // of the build. + const heroThumbLabel = await page.$eval('.card[data-id="challenger-hero"] .inspo figcaption', (el) => el.textContent); await context.close(); assert.equal(textOnlyMedia, null, 'text-only card has no .media region'); assert.ok(textOnlyFace, 'text-only card carries the .text-only face class'); - assert.ok(heroMedia, 'the hero card still renders its .media region'); + assert.equal(heroMedia, null, 'parity demotes the challenger hero from a full-bleed face'); assert.equal(textOnlyBack, null, 'text-only card has no unreachable back face'); assert.match(frontRead, /First viewport/, 'text-only front carries the first viewport fact'); assert.match(frontRead, /The case/, 'text-only front carries the case fact'); - assert.equal(heroLabel, 'inspiration', 'sketchless catalog art is labeled inspiration'); + assert.equal(heroThumbLabel, 'inspired by', 'demoted catalog art is a labeled thumb'); + } finally { + await stopDaemon(cwd, key); + rmSync(cwd, { recursive: true, force: true }); + } + }); + + it('(e2) verdicts route the deck: declined cards demote, reorder to the end, and stay adoptable', async () => { + const cwd = makeWorkspace(); + const key = 'verdicts'; + const hero = makeFakeImage(cwd, 'declined hero', 'declined-hero.png'); + const winnerHero = makeFakeImage(cwd, 'winner hero', 'winner-hero.png'); + const payload = { + title: 'Choose the visual world', + options: [ + { + id: 'assigned', label: 'The Seedsman Catalog', kicker: 'THE ROLL', hero: winnerHero, + raised: [{ from: 'challenger-deepsea', raise: 'The catalog now owns its whole viewport.' }], + }, + // Declined dealt before a competitive card on purpose: the page owns + // the reorder, so payload order cannot promote a demoted world. + { + id: 'challenger-deepsea', label: 'Deep Sea Survey', verdict: 'declined', + case: 'Fuses poorly: buyers do not identify with abyssal instrumentation.', + kept: 'Total environmental commitment.', hero, + // A stray sketch on a declined card must not re-promote it to a + // full media face; the renderer ignores it outright. + sketch: '.impeccable/sketches/challenger-deepsea.webp', + }, + { id: 'challenger-waxprint', label: 'Wax Print Market', verdict: 'competitive', hero: winnerHero }, + ], + reroll: true, steer: true, + }; + const { url } = await startDaemon(cwd, payload, key); + try { + const context = await browser.newContext(); + const page = await context.newPage(); + await page.goto(url, { waitUntil: 'load' }); + await page.waitForSelector('button.choose'); + const order = await page.$$eval('.card', (cards) => cards.map((c) => c.dataset.id)); + const declinedCard = await page.$('.card.declined[data-id="challenger-deepsea"]'); + const declinedMedia = await page.$('.card[data-id="challenger-deepsea"] .media'); + const declinedThumb = await page.$('.card[data-id="challenger-deepsea"] .inspo'); + const declinedFront = await page.$eval('.card[data-id="challenger-deepsea"] .face.front', (el) => el.textContent); + const declinedButton = await page.$eval('.card[data-id="challenger-deepsea"] .face.front button.choose', (el) => el.textContent); + const competitiveMedia = await page.$('.card[data-id="challenger-waxprint"] .media'); + const raise = await page.$eval('.card[data-id="assigned"] .raise', (el) => el.textContent); + // A demoted card is still a real choice: adopting it must answer. + await page.click('.card[data-id="challenger-deepsea"] .face.front button.choose'); + const collected = await waitLoop(cwd, key); + await context.close(); + assert.equal(collected.code, 0, collected.out); + const answer = JSON.parse(collected.out.match(/ANSWER: (\{.*\})/)[1]); + assert.deepEqual(order, ['assigned', 'challenger-waxprint', 'challenger-deepsea'], 'declined cards reorder to the end'); + assert.ok(declinedCard, 'declined verdict adds the .declined card class'); + assert.equal(declinedMedia, null, 'declined catalog art never renders full-bleed'); + assert.ok(declinedThumb, 'declined catalog art rides as a labeled thumb'); + assert.match(declinedFront, /Kept/, 'the declined front carries its kept line'); + assert.equal(declinedButton, 'Adopt anyway', 'the declined action is adopt, not build'); + assert.ok(competitiveMedia, 'a competitive challenger keeps its full media face'); + assert.match(raise, /Raised by Deep Sea Survey/, 'the assigned card names its donor'); + assert.equal(answer.optionId, 'challenger-deepsea', 'adopting a declined card answers with its id'); + } finally { + await stopDaemon(cwd, key); + rmSync(cwd, { recursive: true, force: true }); + } + }); + + it('(e3) register steers ride the re-roll: the bolder button answers with its register', async () => { + const cwd = makeWorkspace(); + const key = 'registers'; + const payload = { + title: 'Choose the visual world', + options: [{ id: 'assigned', label: 'The Seedsman Catalog', kicker: 'THE ROLL' }], + reroll: { registers: ['safer', 'bolder'] }, + steer: true, + }; + const { url } = await startDaemon(cwd, payload, key); + try { + const context = await browser.newContext(); + const page = await context.newPage(); + await page.goto(url, { waitUntil: 'load' }); + await page.waitForSelector('#reroll-bolder'); + assert.ok(await page.$('#reroll-safer'), 'the safer steer renders'); + assert.ok(await page.$('#reroll'), 'the plain re-roll stays between the registers'); + await page.click('#reroll-bolder'); + const collected = await waitLoop(cwd, key); + await context.close(); + assert.equal(collected.code, 0, collected.out); + const answer = JSON.parse(collected.out.match(/ANSWER: (\{.*\})/)[1]); + assert.equal(answer.optionId, 'reroll'); + assert.equal(answer.register, 'bolder', 'the answer names the requested register'); + assert.match(collected.out, /REGISTER: .*bolder/, 'the register directive tells the agent to re-run the seed'); + } finally { + await stopDaemon(cwd, key); + rmSync(cwd, { recursive: true, force: true }); + } + }); + + it('(e4) followup keeps the table open: direction pick, then the execution-contract round', async () => { + const cwd = makeWorkspace(); + const key = 'followup'; + const directionRound = { + title: 'Choose the visual world', + options: [{ id: 'assigned', label: 'The Seedsman Catalog', kicker: 'THE ROLL' }], + reroll: true, steer: true, + followup: true, + }; + const buildPathRound = { + title: 'How should it be built?', + question: 'Same direction, two execution contracts. Pick where the risk goes.', + options: [ + { id: 'comp-led', label: 'Comp-led', kicker: 'BOLD', thesis: 'A first-viewport comp leads and the build matches it.', risk: 'Fix rounds expected; motion arrives last.' }, + { id: 'code-led', label: 'Code-led', kicker: 'CRAFTED', thesis: 'Code authors the page; the boards calibrate it.', risk: 'Composition stays closer to convention.' }, + ], + }; + const { url } = await startDaemon(cwd, directionRound, key); + try { + const context = await browser.newContext(); + const page = await context.newPage(); + await page.goto(url, { waitUntil: 'load' }); + await page.waitForSelector('button.choose'); + await page.click('.card[data-id="assigned"] .face.front button.choose'); + + // First answer: the direction. Not terminal, and it says so. + const first = await waitLoop(cwd, key); + assert.equal(first.code, 0, first.out); + const firstAnswer = JSON.parse(first.out.match(/ANSWER: (\{.*\})/)[1]); + assert.equal(firstAnswer.optionId, 'assigned'); + assert.equal(firstAnswer.followup, true, 'the answer marks the table as still open'); + assert.match(first.out, /FOLLOWUP OPEN:/, 'the directive tells the agent to send the next round'); + assert.ok(existsSync(path.join(cwd, '.impeccable', 'questions', `${key}.state.json`)), + 'server state file survives a followup pick'); + + // The page swapped to the loading hand instead of goodbye. + await page.waitForSelector('.card.skeleton'); + + // Deliver the execution-contract round; the page reloads into it. + const nextPayloadPath = path.join(cwd, 'build-path.json'); + writeFileSync(nextPayloadPath, JSON.stringify(buildPathRound)); + const updated = await run(['--update', '--key', key, '--payload', nextPayloadPath], cwd); + assert.equal(updated.code, 0, updated.out); + await page.waitForSelector('.card[data-id="code-led"]'); + await page.click('.card[data-id="code-led"] .face.front button.choose'); + + // Second answer: terminal, table cleaned up. + const second = await waitLoop(cwd, key); + await context.close(); + assert.equal(second.code, 0, second.out); + const secondAnswer = JSON.parse(second.out.match(/ANSWER: (\{.*\})/)[1]); + assert.equal(secondAnswer.optionId, 'code-led'); + assert.equal(secondAnswer.followup, undefined, 'the build-path round is terminal'); + assert.ok(!existsSync(path.join(cwd, '.impeccable', 'questions', `${key}.state.json`)), + 'the terminal pick removes the server state file'); } finally { await stopDaemon(cwd, key); rmSync(cwd, { recursive: true, force: true }); diff --git a/tests/serve-question.test.mjs b/tests/serve-question.test.mjs index cfee001b0..f6dd97a53 100644 --- a/tests/serve-question.test.mjs +++ b/tests/serve-question.test.mjs @@ -208,11 +208,16 @@ describe('serve-question', () => { canon: true, canonCard: { label: 'The category standard', thesis: 'What the category ships.' }, steer: true, + followup: true, }; const { child, url, read } = await startServer(payload); const html = await (await fetch(url)).text(); // Anatomy renders: chips, tags, fact labels, thesis. assert.match(html, /swatches/); + // A blocking server has no update channel, so even a followup payload + // must not arm the page's loading-hand path (detached mode arms it; the + // new-work e2e suite covers that side). + assert.match(html, /const FOLLOWUP = false/); assert.match(html, /background:#e8452c/); assert.match(html, /class="tag">letterpress/); assert.match(html, /The gig poster idea\./); From d65a08b0644bc2bc9639fcf22b042eadf4136f2d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 21:17:56 +0000 Subject: [PATCH 18/35] Sync generated provider output --- .agents/skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .claude/skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .cursor/skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .gemini/skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .github/skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .grok/skills/impeccable/reference/bolder.md | 2 + .grok/skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .kiro/skills/impeccable/reference/bolder.md | 2 + .kiro/skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .../skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .pi/skills/impeccable/reference/bolder.md | 2 + .pi/skills/impeccable/reference/new-work.md | 12 +- .pi/skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .pi/skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .qoder/skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .../skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .../skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .trae/skills/impeccable/reference/bolder.md | 2 + .trae/skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- .vibe/skills/impeccable/reference/bolder.md | 2 + .vibe/skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- plugin/skills/impeccable/reference/bolder.md | 2 + .../skills/impeccable/reference/new-work.md | 12 +- .../skills/impeccable/reference/visualize.md | 2 +- .../impeccable/scripts/concept-seed.mjs | 195 +++++++++++++++--- .../scripts/lib/impeccable-config.mjs | 98 ++++----- .../skills/impeccable/scripts/live-browser.js | 31 ++- .../scripts/live/browser-script-parts.mjs | 26 ++- .../impeccable/scripts/live/ui-surfaces.mjs | 75 +++++++ .../impeccable/scripts/serve-question.mjs | 195 +++++++++++++++--- 135 files changed, 7365 insertions(+), 2175 deletions(-) create mode 100644 .agents/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .claude/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .cursor/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .gemini/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .github/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .grok/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .kiro/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .opencode/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .pi/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .qoder/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .rovodev/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .trae-cn/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .trae/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 .vibe/skills/impeccable/scripts/live/ui-surfaces.mjs create mode 100644 plugin/skills/impeccable/scripts/live/ui-surfaces.mjs diff --git a/.agents/skills/impeccable/reference/bolder.md b/.agents/skills/impeccable/reference/bolder.md index 9fe39ca59..026c10a6a 100644 --- a/.agents/skills/impeccable/reference/bolder.md +++ b/.agents/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.agents/skills/impeccable/reference/new-work.md b/.agents/skills/impeccable/reference/new-work.md index 755d734e3..8f654b7d7 100644 --- a/.agents/skills/impeccable/reference/new-work.md +++ b/.agents/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .agents/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .agents/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .agents/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .agents/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.agents/skills/impeccable/reference/visualize.md b/.agents/skills/impeccable/reference/visualize.md index 815e29d3a..c20d910f3 100644 --- a/.agents/skills/impeccable/reference/visualize.md +++ b/.agents/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.agents/skills/impeccable/scripts/concept-seed.mjs b/.agents/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.agents/skills/impeccable/scripts/concept-seed.mjs +++ b/.agents/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.agents/skills/impeccable/scripts/lib/impeccable-config.mjs b/.agents/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.agents/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.agents/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.agents/skills/impeccable/scripts/live-browser.js b/.agents/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.agents/skills/impeccable/scripts/live-browser.js +++ b/.agents/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.agents/skills/impeccable/scripts/live/browser-script-parts.mjs b/.agents/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.agents/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.agents/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.agents/skills/impeccable/scripts/live/ui-surfaces.mjs b/.agents/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.agents/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.claude/skills/impeccable/reference/bolder.md b/.claude/skills/impeccable/reference/bolder.md index fced49456..a5c34cd3e 100644 --- a/.claude/skills/impeccable/reference/bolder.md +++ b/.claude/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.claude/skills/impeccable/reference/new-work.md b/.claude/skills/impeccable/reference/new-work.md index 5161d25cb..718b4bd0b 100644 --- a/.claude/skills/impeccable/reference/new-work.md +++ b/.claude/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .claude/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .claude/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .claude/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .claude/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -80,7 +82,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.claude/skills/impeccable/reference/visualize.md b/.claude/skills/impeccable/reference/visualize.md index 94c337f15..6df08b30b 100644 --- a/.claude/skills/impeccable/reference/visualize.md +++ b/.claude/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.claude/skills/impeccable/scripts/concept-seed.mjs b/.claude/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.claude/skills/impeccable/scripts/concept-seed.mjs +++ b/.claude/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.claude/skills/impeccable/scripts/lib/impeccable-config.mjs b/.claude/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.claude/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.claude/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.claude/skills/impeccable/scripts/live-browser.js b/.claude/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.claude/skills/impeccable/scripts/live-browser.js +++ b/.claude/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.claude/skills/impeccable/scripts/live/browser-script-parts.mjs b/.claude/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.claude/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.claude/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.claude/skills/impeccable/scripts/live/ui-surfaces.mjs b/.claude/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.claude/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.cursor/skills/impeccable/reference/bolder.md b/.cursor/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.cursor/skills/impeccable/reference/bolder.md +++ b/.cursor/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.cursor/skills/impeccable/reference/new-work.md b/.cursor/skills/impeccable/reference/new-work.md index 99ed558b5..0dff0aea7 100644 --- a/.cursor/skills/impeccable/reference/new-work.md +++ b/.cursor/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .cursor/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .cursor/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .cursor/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .cursor/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.cursor/skills/impeccable/reference/visualize.md b/.cursor/skills/impeccable/reference/visualize.md index 5877eae67..3a8cc1e4a 100644 --- a/.cursor/skills/impeccable/reference/visualize.md +++ b/.cursor/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.cursor/skills/impeccable/scripts/concept-seed.mjs b/.cursor/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.cursor/skills/impeccable/scripts/concept-seed.mjs +++ b/.cursor/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.cursor/skills/impeccable/scripts/lib/impeccable-config.mjs b/.cursor/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.cursor/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.cursor/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.cursor/skills/impeccable/scripts/live-browser.js b/.cursor/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.cursor/skills/impeccable/scripts/live-browser.js +++ b/.cursor/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.cursor/skills/impeccable/scripts/live/browser-script-parts.mjs b/.cursor/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.cursor/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.cursor/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.cursor/skills/impeccable/scripts/live/ui-surfaces.mjs b/.cursor/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.cursor/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.gemini/skills/impeccable/reference/bolder.md b/.gemini/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.gemini/skills/impeccable/reference/bolder.md +++ b/.gemini/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.gemini/skills/impeccable/reference/new-work.md b/.gemini/skills/impeccable/reference/new-work.md index 77ec7396f..08a8ed6f0 100644 --- a/.gemini/skills/impeccable/reference/new-work.md +++ b/.gemini/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .gemini/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .gemini/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .gemini/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .gemini/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.gemini/skills/impeccable/reference/visualize.md b/.gemini/skills/impeccable/reference/visualize.md index 6dae2cb0b..82f2cc4de 100644 --- a/.gemini/skills/impeccable/reference/visualize.md +++ b/.gemini/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.gemini/skills/impeccable/scripts/concept-seed.mjs b/.gemini/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.gemini/skills/impeccable/scripts/concept-seed.mjs +++ b/.gemini/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.gemini/skills/impeccable/scripts/lib/impeccable-config.mjs b/.gemini/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.gemini/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.gemini/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.gemini/skills/impeccable/scripts/live-browser.js b/.gemini/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.gemini/skills/impeccable/scripts/live-browser.js +++ b/.gemini/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.gemini/skills/impeccable/scripts/live/browser-script-parts.mjs b/.gemini/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.gemini/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.gemini/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.gemini/skills/impeccable/scripts/live/ui-surfaces.mjs b/.gemini/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.gemini/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.github/skills/impeccable/reference/bolder.md b/.github/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.github/skills/impeccable/reference/bolder.md +++ b/.github/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.github/skills/impeccable/reference/new-work.md b/.github/skills/impeccable/reference/new-work.md index a249c27ad..959eb081e 100644 --- a/.github/skills/impeccable/reference/new-work.md +++ b/.github/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .github/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .github/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .github/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .github/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.github/skills/impeccable/reference/visualize.md b/.github/skills/impeccable/reference/visualize.md index 285a4c996..60ee2020a 100644 --- a/.github/skills/impeccable/reference/visualize.md +++ b/.github/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.github/skills/impeccable/scripts/concept-seed.mjs b/.github/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.github/skills/impeccable/scripts/concept-seed.mjs +++ b/.github/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.github/skills/impeccable/scripts/lib/impeccable-config.mjs b/.github/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.github/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.github/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.github/skills/impeccable/scripts/live-browser.js b/.github/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.github/skills/impeccable/scripts/live-browser.js +++ b/.github/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.github/skills/impeccable/scripts/live/browser-script-parts.mjs b/.github/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.github/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.github/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.github/skills/impeccable/scripts/live/ui-surfaces.mjs b/.github/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.github/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.grok/skills/impeccable/reference/bolder.md b/.grok/skills/impeccable/reference/bolder.md index fced49456..a5c34cd3e 100644 --- a/.grok/skills/impeccable/reference/bolder.md +++ b/.grok/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.grok/skills/impeccable/reference/new-work.md b/.grok/skills/impeccable/reference/new-work.md index 45027be6e..0e92394b7 100644 --- a/.grok/skills/impeccable/reference/new-work.md +++ b/.grok/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .grok/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .grok/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .grok/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .grok/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.grok/skills/impeccable/reference/visualize.md b/.grok/skills/impeccable/reference/visualize.md index 4dd8b3525..063520a6e 100644 --- a/.grok/skills/impeccable/reference/visualize.md +++ b/.grok/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.grok/skills/impeccable/scripts/concept-seed.mjs b/.grok/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.grok/skills/impeccable/scripts/concept-seed.mjs +++ b/.grok/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.grok/skills/impeccable/scripts/lib/impeccable-config.mjs b/.grok/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.grok/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.grok/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.grok/skills/impeccable/scripts/live-browser.js b/.grok/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.grok/skills/impeccable/scripts/live-browser.js +++ b/.grok/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.grok/skills/impeccable/scripts/live/browser-script-parts.mjs b/.grok/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.grok/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.grok/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.grok/skills/impeccable/scripts/live/ui-surfaces.mjs b/.grok/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.grok/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.kiro/skills/impeccable/reference/bolder.md b/.kiro/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.kiro/skills/impeccable/reference/bolder.md +++ b/.kiro/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.kiro/skills/impeccable/reference/new-work.md b/.kiro/skills/impeccable/reference/new-work.md index 479e37b10..2402bc90a 100644 --- a/.kiro/skills/impeccable/reference/new-work.md +++ b/.kiro/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .kiro/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .kiro/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .kiro/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .kiro/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.kiro/skills/impeccable/reference/visualize.md b/.kiro/skills/impeccable/reference/visualize.md index f298c6c69..cc0057342 100644 --- a/.kiro/skills/impeccable/reference/visualize.md +++ b/.kiro/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.kiro/skills/impeccable/scripts/concept-seed.mjs b/.kiro/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.kiro/skills/impeccable/scripts/concept-seed.mjs +++ b/.kiro/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.kiro/skills/impeccable/scripts/lib/impeccable-config.mjs b/.kiro/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.kiro/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.kiro/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.kiro/skills/impeccable/scripts/live-browser.js b/.kiro/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.kiro/skills/impeccable/scripts/live-browser.js +++ b/.kiro/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.kiro/skills/impeccable/scripts/live/browser-script-parts.mjs b/.kiro/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.kiro/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.kiro/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.kiro/skills/impeccable/scripts/live/ui-surfaces.mjs b/.kiro/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.kiro/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.opencode/skills/impeccable/reference/bolder.md b/.opencode/skills/impeccable/reference/bolder.md index 5408e49d0..1055d6a9f 100644 --- a/.opencode/skills/impeccable/reference/bolder.md +++ b/.opencode/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.opencode/skills/impeccable/reference/new-work.md b/.opencode/skills/impeccable/reference/new-work.md index 290c53138..95aa35654 100644 --- a/.opencode/skills/impeccable/reference/new-work.md +++ b/.opencode/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .opencode/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .opencode/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .opencode/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .opencode/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.opencode/skills/impeccable/reference/visualize.md b/.opencode/skills/impeccable/reference/visualize.md index 43aebde6e..5bd14ae50 100644 --- a/.opencode/skills/impeccable/reference/visualize.md +++ b/.opencode/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.opencode/skills/impeccable/scripts/concept-seed.mjs b/.opencode/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.opencode/skills/impeccable/scripts/concept-seed.mjs +++ b/.opencode/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.opencode/skills/impeccable/scripts/lib/impeccable-config.mjs b/.opencode/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.opencode/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.opencode/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.opencode/skills/impeccable/scripts/live-browser.js b/.opencode/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.opencode/skills/impeccable/scripts/live-browser.js +++ b/.opencode/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.opencode/skills/impeccable/scripts/live/browser-script-parts.mjs b/.opencode/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.opencode/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.opencode/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.opencode/skills/impeccable/scripts/live/ui-surfaces.mjs b/.opencode/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.opencode/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.pi/skills/impeccable/reference/bolder.md b/.pi/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.pi/skills/impeccable/reference/bolder.md +++ b/.pi/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.pi/skills/impeccable/reference/new-work.md b/.pi/skills/impeccable/reference/new-work.md index 57437e7ba..ffc87a930 100644 --- a/.pi/skills/impeccable/reference/new-work.md +++ b/.pi/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .pi/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .pi/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .pi/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .pi/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.pi/skills/impeccable/reference/visualize.md b/.pi/skills/impeccable/reference/visualize.md index 0608624d9..539dd0286 100644 --- a/.pi/skills/impeccable/reference/visualize.md +++ b/.pi/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.pi/skills/impeccable/scripts/concept-seed.mjs b/.pi/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.pi/skills/impeccable/scripts/concept-seed.mjs +++ b/.pi/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.pi/skills/impeccable/scripts/lib/impeccable-config.mjs b/.pi/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.pi/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.pi/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.pi/skills/impeccable/scripts/live-browser.js b/.pi/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.pi/skills/impeccable/scripts/live-browser.js +++ b/.pi/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.pi/skills/impeccable/scripts/live/browser-script-parts.mjs b/.pi/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.pi/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.pi/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.pi/skills/impeccable/scripts/live/ui-surfaces.mjs b/.pi/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.pi/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.qoder/skills/impeccable/reference/bolder.md b/.qoder/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.qoder/skills/impeccable/reference/bolder.md +++ b/.qoder/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.qoder/skills/impeccable/reference/new-work.md b/.qoder/skills/impeccable/reference/new-work.md index 983ce2b96..c0ce92c06 100644 --- a/.qoder/skills/impeccable/reference/new-work.md +++ b/.qoder/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .qoder/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .qoder/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .qoder/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .qoder/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.qoder/skills/impeccable/reference/visualize.md b/.qoder/skills/impeccable/reference/visualize.md index 7ccc0af4c..3e6a3a7e9 100644 --- a/.qoder/skills/impeccable/reference/visualize.md +++ b/.qoder/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.qoder/skills/impeccable/scripts/concept-seed.mjs b/.qoder/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.qoder/skills/impeccable/scripts/concept-seed.mjs +++ b/.qoder/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.qoder/skills/impeccable/scripts/lib/impeccable-config.mjs b/.qoder/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.qoder/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.qoder/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.qoder/skills/impeccable/scripts/live-browser.js b/.qoder/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.qoder/skills/impeccable/scripts/live-browser.js +++ b/.qoder/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.qoder/skills/impeccable/scripts/live/browser-script-parts.mjs b/.qoder/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.qoder/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.qoder/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.qoder/skills/impeccable/scripts/live/ui-surfaces.mjs b/.qoder/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.qoder/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.rovodev/skills/impeccable/reference/bolder.md b/.rovodev/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.rovodev/skills/impeccable/reference/bolder.md +++ b/.rovodev/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.rovodev/skills/impeccable/reference/new-work.md b/.rovodev/skills/impeccable/reference/new-work.md index 9b3edee90..c4a7821fc 100644 --- a/.rovodev/skills/impeccable/reference/new-work.md +++ b/.rovodev/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .rovodev/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .rovodev/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .rovodev/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .rovodev/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.rovodev/skills/impeccable/reference/visualize.md b/.rovodev/skills/impeccable/reference/visualize.md index ad1f4a548..665e8c71c 100644 --- a/.rovodev/skills/impeccable/reference/visualize.md +++ b/.rovodev/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.rovodev/skills/impeccable/scripts/concept-seed.mjs b/.rovodev/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.rovodev/skills/impeccable/scripts/concept-seed.mjs +++ b/.rovodev/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.rovodev/skills/impeccable/scripts/lib/impeccable-config.mjs b/.rovodev/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.rovodev/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.rovodev/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.rovodev/skills/impeccable/scripts/live-browser.js b/.rovodev/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.rovodev/skills/impeccable/scripts/live-browser.js +++ b/.rovodev/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.rovodev/skills/impeccable/scripts/live/browser-script-parts.mjs b/.rovodev/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.rovodev/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.rovodev/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.rovodev/skills/impeccable/scripts/live/ui-surfaces.mjs b/.rovodev/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.rovodev/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.trae-cn/skills/impeccable/reference/bolder.md b/.trae-cn/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.trae-cn/skills/impeccable/reference/bolder.md +++ b/.trae-cn/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.trae-cn/skills/impeccable/reference/new-work.md b/.trae-cn/skills/impeccable/reference/new-work.md index f47a72b20..529b63dbc 100644 --- a/.trae-cn/skills/impeccable/reference/new-work.md +++ b/.trae-cn/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .trae-cn/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .trae-cn/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .trae-cn/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .trae-cn/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.trae-cn/skills/impeccable/reference/visualize.md b/.trae-cn/skills/impeccable/reference/visualize.md index 0780bbf8b..6d39ce00a 100644 --- a/.trae-cn/skills/impeccable/reference/visualize.md +++ b/.trae-cn/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.trae-cn/skills/impeccable/scripts/concept-seed.mjs b/.trae-cn/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.trae-cn/skills/impeccable/scripts/concept-seed.mjs +++ b/.trae-cn/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.trae-cn/skills/impeccable/scripts/lib/impeccable-config.mjs b/.trae-cn/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.trae-cn/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.trae-cn/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.trae-cn/skills/impeccable/scripts/live-browser.js b/.trae-cn/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.trae-cn/skills/impeccable/scripts/live-browser.js +++ b/.trae-cn/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.trae-cn/skills/impeccable/scripts/live/browser-script-parts.mjs b/.trae-cn/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.trae-cn/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.trae-cn/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.trae-cn/skills/impeccable/scripts/live/ui-surfaces.mjs b/.trae-cn/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.trae-cn/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.trae/skills/impeccable/reference/bolder.md b/.trae/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.trae/skills/impeccable/reference/bolder.md +++ b/.trae/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.trae/skills/impeccable/reference/new-work.md b/.trae/skills/impeccable/reference/new-work.md index f70c82584..5c805afc8 100644 --- a/.trae/skills/impeccable/reference/new-work.md +++ b/.trae/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .trae/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .trae/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .trae/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .trae/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.trae/skills/impeccable/reference/visualize.md b/.trae/skills/impeccable/reference/visualize.md index ef675864c..247ea2067 100644 --- a/.trae/skills/impeccable/reference/visualize.md +++ b/.trae/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.trae/skills/impeccable/scripts/concept-seed.mjs b/.trae/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.trae/skills/impeccable/scripts/concept-seed.mjs +++ b/.trae/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.trae/skills/impeccable/scripts/lib/impeccable-config.mjs b/.trae/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.trae/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.trae/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.trae/skills/impeccable/scripts/live-browser.js b/.trae/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.trae/skills/impeccable/scripts/live-browser.js +++ b/.trae/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.trae/skills/impeccable/scripts/live/browser-script-parts.mjs b/.trae/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.trae/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.trae/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.trae/skills/impeccable/scripts/live/ui-surfaces.mjs b/.trae/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.trae/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/.vibe/skills/impeccable/reference/bolder.md b/.vibe/skills/impeccable/reference/bolder.md index 78f5e4811..c5446cfe0 100644 --- a/.vibe/skills/impeccable/reference/bolder.md +++ b/.vibe/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/.vibe/skills/impeccable/reference/new-work.md b/.vibe/skills/impeccable/reference/new-work.md index bda245059..4adf881d6 100644 --- a/.vibe/skills/impeccable/reference/new-work.md +++ b/.vibe/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .vibe/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .vibe/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .vibe/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .vibe/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -78,7 +80,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/.vibe/skills/impeccable/reference/visualize.md b/.vibe/skills/impeccable/reference/visualize.md index 87e410295..a8bed5230 100644 --- a/.vibe/skills/impeccable/reference/visualize.md +++ b/.vibe/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/.vibe/skills/impeccable/scripts/concept-seed.mjs b/.vibe/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/.vibe/skills/impeccable/scripts/concept-seed.mjs +++ b/.vibe/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/.vibe/skills/impeccable/scripts/lib/impeccable-config.mjs b/.vibe/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/.vibe/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/.vibe/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/.vibe/skills/impeccable/scripts/live-browser.js b/.vibe/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/.vibe/skills/impeccable/scripts/live-browser.js +++ b/.vibe/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/.vibe/skills/impeccable/scripts/live/browser-script-parts.mjs b/.vibe/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/.vibe/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.vibe/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/.vibe/skills/impeccable/scripts/live/ui-surfaces.mjs b/.vibe/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/.vibe/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } diff --git a/plugin/skills/impeccable/reference/bolder.md b/plugin/skills/impeccable/reference/bolder.md index fced49456..a5c34cd3e 100644 --- a/plugin/skills/impeccable/reference/bolder.md +++ b/plugin/skills/impeccable/reference/bolder.md @@ -1,5 +1,7 @@ > **Additional context needed**: which section is the target, and what must stay untouched. +An open direction round owns the word first: "bolder" said while a direction decision is on the table is the Bolder hand register steer, a fresh deal of foreign forms (see new-work.md), not this command. This command refines a surface whose world already shipped. + "Bolder" is an amplification request, and almost always it is scoped to something that already exists. The surrounding page, its system, and its conventions are the given. Your job is to raise one part to the conviction the rest already implies, without rebuilding anything the brief did not name. The reflex answer, reaching for more effects, is the opposite of bold; reject it first. ## Scope is sovereign diff --git a/plugin/skills/impeccable/reference/new-work.md b/plugin/skills/impeccable/reference/new-work.md index 5161d25cb..718b4bd0b 100644 --- a/plugin/skills/impeccable/reference/new-work.md +++ b/plugin/skills/impeccable/reference/new-work.md @@ -43,12 +43,14 @@ The script assigns which structure gets built; your top-ranked structure is what 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; name both as the rut and keep them out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily; a nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. -4. Run `node .claude/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. Never present a ranked menu of your own grounded candidates; a lineup of those invites the safest card. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +4. Run `node .claude/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode ` and follow what it prints. This step has no substitute and no skip condition: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure, because the roll is the mechanism that keeps every run from converging on the category default. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. The weighing closes with a verdict per challenger, decided before any borrowing is considered: wins (beats the assigned direction on both axes; it becomes the build candidate), competitive (holds one axis; it stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a motif lifted from a declined world is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. +5. Present one direction, fully committed and already raised by the hand it beat, its raises visible as named lines: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, route each dealt challenger by its verdict: winning and competitive challengers are full alternates carrying their QUALITY BAR cards and one-line case, while declined challengers render demoted, compact and quiet, each carrying its verdict plus what the direction kept from it, never full-size and never silently dropped, each still adoptable on request. The verdict informs the user's choice, it never pre-empts it; the demoted row is the hand's proof of judgment, showing why the dealt worlds made the presented direction better. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker MY PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often the one most runs in this category land on, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: the rest of your grounded candidates stay yours, because a lineup of them hands selection back to a taste function and invites the safest card. The pick never takes the lead position, and when the dice assign your top candidate there is no pick card; the assigned card notes it also topped your list. Add re-roll with an optional one-line steer, offered in three registers: plain (a fresh hand, same spread), safer (the familiar register: your remaining conventional grounded candidates plus the canon against named competitors), and bolder (foreign forms only, at full commitment). A register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit as its last option, while declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel too. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .claude/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .claude/skills/impeccable/scripts/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. + +The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. @@ -80,7 +82,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -Whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. diff --git a/plugin/skills/impeccable/reference/visualize.md b/plugin/skills/impeccable/reference/visualize.md index 94c337f15..6df08b30b 100644 --- a/plugin/skills/impeccable/reference/visualize.md +++ b/plugin/skills/impeccable/reference/visualize.md @@ -1,6 +1,6 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. diff --git a/plugin/skills/impeccable/scripts/concept-seed.mjs b/plugin/skills/impeccable/scripts/concept-seed.mjs index aab9e8911..5b4345818 100644 --- a/plugin/skills/impeccable/scripts/concept-seed.mjs +++ b/plugin/skills/impeccable/scripts/concept-seed.mjs @@ -31,6 +31,16 @@ * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. + * - REGISTER (--register safer|bolder): the user's steering on the + * familiar-to-bold axis, applied to a re-roll round. A register changes + * only what this round instructs, never what it dealt: the same key and + * reroll count reproduce the same deal whatever the register, so the + * exclusion chain never forks. bolder presents the dealt foreign forms + * as the whole hand (first-dealt leads, dice-assigned by deal order); + * safer spends the dealt hand unseen and presents the familiar register, + * the model's conventional grounded candidates plus the canon against + * named competitors, the one sanctioned lineup of the model's own list. + * Registers are user-requested, never pre-selected by the model. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself * is unchanged. @@ -41,7 +51,9 @@ * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow * node scripts/concept-seed.mjs --scope direction --candidate-count 6 * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 - * node scripts/concept-seed.mjs --chosen --from --scope direction + * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder + * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction + * node scripts/concept-seed.mjs --kind assigned --from --scope direction * * --grain names how much of the product is in play: product, flow, view, or * region. A docs site, an onboarding flow, a landing page and a data table are @@ -62,8 +74,13 @@ * Challenger data resolves in order: a local catalog directory (the private * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll * API at impeccable.style, then a degraded assignment-only seed when both are - * unavailable. --chosen sends the anonymous choice ping for API-dealt rolls; - * DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables it. + * unavailable. The anonymous choice ping fires once per resolved attended + * round on API-dealt rolls: --kind names which card class won (assigned, + * pick, challenger, canon) so share metrics have a denominator, --chosen + * carries the catalog id when a dealt challenger won, and --register rides + * along when the round came from a steered hand. Grounded candidates' names + * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables + * the ping entirely. * * Env vars: * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs. @@ -172,17 +189,35 @@ function telemetryDisabled() { return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK); } -// Anonymous choice ping: records only that a dealt world was selected. +// Anonymous choice ping: one per resolved attended direction round. kind +// says which card class won (assigned / pick / challenger / canon), so +// pick-share and canon-share have a denominator; chosenId rides along only +// when a dealt catalog world won, and register only when the round came from +// a steered hand. Grounded candidates' names never leave the machine: they +// are derived from the user's project, so the ping carries the kind alone. // Fire-and-forget; never fails the caller. -export async function pingChosen({ chosenId, key, scope, mode }) { - if (telemetryDisabled() || !chosenId) return false; +const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']); +export async function pingChosen({ chosenId, key, scope, mode, kind, register }) { + if (telemetryDisabled()) return false; + if (kind && !PING_KINDS.has(kind)) return false; + if (register && register !== 'safer' && register !== 'bolder') return false; + // Legacy shape: a bare challenger id with no kind stays a valid ping. + if (!chosenId && !kind) return false; + if ((kind === 'challenger' || !kind) && !chosenId) return false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), apiBudgetMs()); try { await fetch(`${API_BASE}/chosen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chosenId, key, scope, mode }), + body: JSON.stringify({ + ...(chosenId ? { chosenId } : {}), + key, + scope, + mode, + ...(kind ? { kind } : {}), + ...(register ? { register } : {}), + }), signal: controller.signal, }); return true; @@ -260,6 +295,7 @@ export function renderConceptSeed({ scope = 'surface', key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'), reroll = 0, + register = null, mode = null, grain = null, platform = null, @@ -273,6 +309,15 @@ export function renderConceptSeed({ if (!Number.isInteger(reroll) || reroll < 0) { throw new Error('concept-seed: --reroll must be a non-negative integer'); } + if (register !== null && register !== 'safer' && register !== 'bolder') { + throw new Error('concept-seed: --register must be safer or bolder'); + } + if (register !== null && reroll < 1) { + throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it'); + } + if (register !== null && scope !== 'direction') { + throw new Error('concept-seed: --register applies to direction rounds only'); + } if (mode !== null && !SEED_MODES.has(mode)) { throw new Error('concept-seed: --mode must be persuade, operate, read, or experience'); } @@ -326,6 +371,7 @@ export function renderConceptSeed({ scope, key, reroll, + register, mode, grain, platform, @@ -357,7 +403,11 @@ export function renderConceptSeed({ survive the current task plus navigation, quiet and dense content, interaction and state, and a substantially different future surface. In an attended run, present the assigned direction fully committed and offer - re-roll; never present a ranked lineup to choose from. Re-roll yourself only + re-roll. You may add ONE card for your top-ranked grounded candidate when + it is not the assigned direction, kicker MY PICK, with an honest risk line + naming its familiarity; one pick card, never a ranked lineup, and the pick + never takes the lead position. When the assignment IS your top candidate, + there is no pick card. Re-roll yourself only on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, @@ -374,7 +424,16 @@ export function renderConceptSeed({ conflicts. Weigh the fused result against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture - list is the point. A fused challenger that wins both axes becomes the build.` + list is the point. A fused challenger that wins both axes becomes the build. + Close the weighing with a verdict per challenger, decided before any + borrowing is considered: wins (beats the assigned direction on both axes), + competitive (holds one axis), or declined (loses both). A declined + challenger is not spent: name the one discipline of its system the assigned + direction lacks, and raise the assigned direction to match before + presenting it. A donation transfers ambition and system discipline, never + the challenger's clothes; one world owns the page. Write each raise as its + own named line on the presented direction, and carry every verdict, kept + line, and raise into the decision page payload.` : `A challenger wins only when its fused result beats the grounded list on audience identification and product clarity. It may change task topology or interaction, but never the committed visual identity.`; @@ -399,8 +458,39 @@ Ambitious motion, spatial media, or interaction is welcome when it strengthens the product without weakening semantics, performance, or fallback behavior.`; if (!data) { - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount}) -ASSIGNED INDEX: ${buildIndex} + // A degraded roll can still serve the safer register, which needs no + // catalog at all: the assignment machinery is suppressed entirely, the + // same as the non-degraded safer round, because emitting both "the user + // picks" and a mandatory numbered build order hands the model two + // contradicting instructions and the mandatory one tends to win. The + // bolder register is exactly the thing degradation took away, so it + // falls back to a plain grounded round, disclosed. + const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`; + if (register === 'safer') { + return `${degradedHeader} +SAFER REGISTER (user-requested): the assigned index is suspended this + round; the user picks, and no candidate is mandated. Present the familiar + register: your remaining grounded candidates from the conventional end, at + most three, as full cards with an honest risk line each, plus the canon + executed against two or three named competitors. This is the one sanctioned + lineup of your own ranked candidates; it exists only by this explicit + request. When the user voices a standing preference for it, record a brand + commitment in PRODUCT.md. +${authorityInstruction} +A user- or brief-pinned decision beats the roll, always. +REGISTER (restated for truncated readers): safer, user-requested; the +assigned index is suspended this round and the user picks; seed key ${key}. +`; + } + const degradedRegister = register === 'bolder' + ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran + degraded with no catalog and no roll service, so there is nothing bold to + deal. Tell the user, then run this round as a plain grounded re-roll; the + assignment below applies. +` + : ''; + return `${degradedHeader} +${degradedRegister}ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -471,34 +561,76 @@ structure only, never a palette, typeface, or material. Treat them as serious rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n` : ''; const rerollBlock = reroll > 0 - ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded - and challenger alike, is eliminated and may not return reworded. Derive + ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded + and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive genuinely new grounded candidates from unexplored angles before judging - these fresh challengers.\n` + these fresh challengers.`}\n` : ''; + // A register swaps the round's presentation, never its deal: the assigned + // index and challenger fetch stay identical so the chain reproduces, and + // only the instructions change. + const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this + round's dealt hand is spent unseen, stays excluded from future rounds, and + is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded + candidates from the conventional end, at most three, as full cards with an + honest risk line each, plus the canon executed against two or three named + competitors. This is the one sanctioned lineup of your own ranked + candidates; it exists only by this explicit request. When the user voices a + standing preference for it, record a brand commitment in PRODUCT.md.`; + const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no + grounded direction is presented this round and the assigned index is + suspended. The hand is every dealt challenger below, each fused with the + product and presented as a full card; the FIRST dealt challenger leads, an + assignment by deal order, so the dice still choose. Verdicts and donations + apply between the challengers, weighed against the leader. The pick card + sits out; the canon stays, as always.`; const telemetryBlock = data.source === 'api' - ? `TELEMETRY: if the resolved direction uses one of these challengers, rerun - this script once with --chosen --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} - after resolution. The ping is anonymous (chosen id only) and is skipped - automatically when DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY is set.\n` + ? `TELEMETRY: after the user's choice resolves, rerun this script once with + --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''}, + adding --chosen when a dealt challenger won and keeping + --register when the resolved round came from a steered hand. + One ping per resolved attended round. The ping is anonymous, the card kind + plus the catalog id when one won; your grounded candidates' names never + leave the machine, and the ping is skipped automatically when DO_NOT_TRACK + or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; - return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) -${rerollBlock}ASSIGNED INDEX: ${buildIndex} + const assignedBlock = register === null + ? `ASSIGNED INDEX: ${buildIndex} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank - the user or the brief. Never expose assignment metadata in user-facing labels. -CHALLENGERS: + the user or the brief. Never expose assignment metadata in user-facing labels.` + : register === 'safer' ? saferBlock : bolderBlock; + // A bolder round has no assigned grounded direction, so the generic + // weighing instruction (which measures against the assignment) would + // contradict the register; the bolder variant weighs against the leader. + const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form + and its system grammar, the product supplies every fact, and clarity wins + conflicts. Weigh every fused challenger against the fused LEADER, the first + dealt, on exactly two axes, audience identification and product clarity; + verdicts and donations apply between the challengers, and one that beats + the leader on both axes presents as the hand's strongest alternate.`; + const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction; + const challengerSection = register === 'safer' + ? '' + : `CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${compositionBlock}${challengerInstruction} +${compositionBlock}${roundChallengerInstruction} When you can view images, open the QUALITY BAR board and hero for any challenger you weigh seriously and for the world you build. They exist as a craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. -${authorityInstruction} +`; + const restated = register === null + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `REGISTER (restated for truncated readers): ${register}, user-requested; the +assigned index is suspended this round; seed key ${key}.`; + return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) +${rerollBlock}${assignedBlock} +${challengerSection}${authorityInstruction} ${richnessInstruction} ${telemetryBlock}A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${restated} `; } @@ -507,19 +639,25 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur const fromIdx = args.indexOf('--from'); const scopeIdx = args.indexOf('--scope'); const rerollIdx = args.indexOf('--reroll'); + const registerIdx = args.indexOf('--register'); const modeIdx = args.indexOf('--mode'); const grainIdx = args.indexOf('--grain'); const platformIdx = args.indexOf('--platform'); const candidateCountIdx = args.indexOf('--candidate-count'); const chosenIdx = args.indexOf('--chosen'); + const kindIdx = args.indexOf('--kind'); try { - if (chosenIdx !== -1) { + if (chosenIdx !== -1 || kindIdx !== -1) { // Choice ping: always exits 0, telemetry must never fail a design flow. + // --kind alone pings a non-challenger outcome (assigned/pick/canon); + // --chosen alone stays the legacy challenger-win ping. const sent = await pingChosen({ - chosenId: args[chosenIdx + 1], + chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined, key: fromIdx !== -1 ? args[fromIdx + 1] : undefined, scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined, mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined, + kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined, + register: registerIdx !== -1 ? args[registerIdx + 1] : undefined, }); process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n'); } else { @@ -542,6 +680,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur ? args[fromIdx + 1] : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')), reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0, + register: registerIdx !== -1 ? args[registerIdx + 1] : null, mode: modeIdx !== -1 ? args[modeIdx + 1] : null, grain: grainIdx !== -1 ? args[grainIdx + 1] : null, platform: platformIdx !== -1 ? args[platformIdx + 1] : null, diff --git a/plugin/skills/impeccable/scripts/lib/impeccable-config.mjs b/plugin/skills/impeccable/scripts/lib/impeccable-config.mjs index 0c052d264..827b26845 100644 --- a/plugin/skills/impeccable/scripts/lib/impeccable-config.mjs +++ b/plugin/skills/impeccable/scripts/lib/impeccable-config.mjs @@ -206,10 +206,10 @@ function parseIgnoreColor(value) { if (rgb) { const parts = splitColorArgs(rgb[1]); if (parts.length < 3 || parts.length > 4) return null; - const r = parseRgbChannel(parts[0]); - const g = parseRgbChannel(parts[1]); - const b = parseRgbChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const r = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.rgb); + const g = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.rgb); + const b = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.rgb); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([r, g, b, a].some((v) => v === null)) return null; return { r, g, b, a }; } @@ -218,10 +218,10 @@ function parseIgnoreColor(value) { if (hsl) { const parts = splitColorArgs(hsl[1]); if (parts.length < 3 || parts.length > 4) return null; - const h = parseHueChannel(parts[0]); - const s = parsePercentChannel(parts[1]); - const l = parsePercentChannel(parts[2]); - const a = parts[3] === undefined ? 1 : parseAlphaChannel(parts[3]); + const h = parseColorChannel(parts[0], COLOR_CHANNEL_FORMATS.hue); + const s = parseColorChannel(parts[1], COLOR_CHANNEL_FORMATS.percent); + const l = parseColorChannel(parts[2], COLOR_CHANNEL_FORMATS.percent); + const a = parts[3] === undefined ? 1 : parseColorChannel(parts[3], COLOR_CHANNEL_FORMATS.alpha); if ([h, s, l, a].some((v) => v === null)) return null; return hslToRgb(h, s, l, a); } @@ -230,18 +230,13 @@ function parseIgnoreColor(value) { } function parseHexIgnoreColor(hex) { - if (hex.length === 3 || hex.length === 4) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - const a = hex.length === 4 ? parseInt(hex[3] + hex[3], 16) / 255 : 1; - return { r, g, b, a }; - } - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; - return { r, g, b, a }; + const expanded = hex.length <= 4 + ? [...hex].map((digit) => digit.repeat(2)).join('') + : hex; + const [r, g, b, alpha = 255] = expanded + .match(/../g) + .map((channel) => Number.parseInt(channel, 16)); + return { r, g, b, a: alpha / 255 }; } function splitColorArgs(body) { @@ -259,47 +254,34 @@ function splitColorArgs(body) { return text.replace(/\s*\/\s*/g, ' / ').split(/\s+/).filter((part) => part && part !== '/'); } -function parseRgbChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const scaled = match[2] ? value * 2.55 : value; - if (scaled < 0 || scaled > 255) return null; - return Math.round(scaled); -} +const CSS_NUMBER_RE = /^(-?\d*\.?\d+)(%|deg|rad|turn|grad)?$/; +const identity = (value) => value; +const COLOR_CHANNEL_FORMATS = { + rgb: { units: { '': identity, '%': (value) => value * 2.55 }, min: 0, max: 255, round: true }, + alpha: { units: { '': identity, '%': (value) => value / 100 }, min: 0, max: 1 }, + hue: { + units: { + '': identity, + deg: identity, + rad: (value) => value * (180 / Math.PI), + turn: (value) => value * 360, + grad: (value) => value * 0.9, + }, + }, + percent: { units: { '%': (value) => value / 100 }, min: 0, max: 1 }, +}; -function parseAlphaChannel(raw) { +function parseColorChannel(raw, { units, min = -Infinity, max = Infinity, round = false }) { const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(%)?$/); + const match = text.match(CSS_NUMBER_RE); if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const alpha = match[2] ? value / 100 : value; - return alpha >= 0 && alpha <= 1 ? alpha : null; -} - -function parseHueChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)?$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - const unit = match[2] || 'deg'; - if (unit === 'turn') return value * 360; - if (unit === 'rad') return value * (180 / Math.PI); - if (unit === 'grad') return value * 0.9; - return value; -} - -function parsePercentChannel(raw) { - const text = String(raw || '').trim(); - const match = text.match(/^(-?\d*\.?\d+)%$/); - if (!match) return null; - const value = Number.parseFloat(match[1]); - if (!Number.isFinite(value)) return null; - return value >= 0 && value <= 100 ? value / 100 : null; + const convert = units[match[2] || '']; + if (!convert) return null; + const number = Number.parseFloat(match[1]); + if (!Number.isFinite(number)) return null; + const value = convert(number); + if (value < min || value > max) return null; + return round ? Math.round(value) : value; } function hslToRgb(hue, saturation, lightness, alpha) { diff --git a/plugin/skills/impeccable/scripts/live-browser.js b/plugin/skills/impeccable/scripts/live-browser.js index aa9bd759b..918dfe093 100644 --- a/plugin/skills/impeccable/scripts/live-browser.js +++ b/plugin/skills/impeccable/scripts/live-browser.js @@ -97,23 +97,20 @@ return { value: c.value, label: c.label }; }); - const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions']; - const LIVE_UI_SURFACES = [ - { key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] }, - { key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] }, - { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] }, - { key: 'action-picker', ids: [PREFIX + '-picker'] }, - { key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] }, - { key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] }, - { key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] }, - { key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] }, - { key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] }, - { key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] }, - { key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] }, - { key: 'design-system-panel', ids: [PREFIX + '-design-host'] }, - { key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] }, - { key: 'css-isolation-boundary', ids: [PREFIX + '-root'] }, - ]; + // The Live chrome inventory (which surfaces exist, and the element ids each + // one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs, + // which the /live.js assembler serializes into these globals alongside the + // token/port/vocabulary. This file is served raw and injected as a classic + // script, so it cannot import that module; the private impeccable-site repo + // imports it directly to check its Live UI lab holds a snapshot for every + // surface, which only works while the list has exactly one definition. + // Add a surface in ui-surfaces.mjs, not here. + const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__) + ? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ + : ['root', 'transport', 'state', 'actions']; + const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__) + ? window.__IMPECCABLE_LIVE_UI_SURFACES__ + : []; const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))]; // diff --git a/plugin/skills/impeccable/scripts/live/browser-script-parts.mjs b/plugin/skills/impeccable/scripts/live/browser-script-parts.mjs index 5925136fb..720709a99 100644 --- a/plugin/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/plugin/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; +import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs'; + export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([ Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }), Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }), @@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) { +export function assembleLiveBrowserScript({ + token, + port, + vocabulary, + commandPrefix = '/', + appRoot = null, + parts, + // Defaulted rather than threaded through live-server.mjs: the browser bundle + // must always carry the canonical inventory, and a default makes that true by + // construction instead of by every caller remembering to pass it. Overridable + // so tests can assemble with a stand-in. + uiSurfaces = LIVE_UI_SURFACES, + mountContract = LIVE_CHROME_MOUNT_CONTRACT, +}) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + @@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. - `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; + `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` + + // Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js + // is a classic script and cannot import an ES module at runtime, so the list + // is serialized here and read off the global there. Node consumers (this + // repo's tests, the impeccable-site Live UI lab) import the module directly, + // which is what keeps the two from drifting. + `window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` + + `window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`; const body = parts.map((part) => { const file = part.file || path.basename(part.path || ''); diff --git a/plugin/skills/impeccable/scripts/live/ui-surfaces.mjs b/plugin/skills/impeccable/scripts/live/ui-surfaces.mjs new file mode 100644 index 000000000..b39ca5846 --- /dev/null +++ b/plugin/skills/impeccable/scripts/live/ui-surfaces.mjs @@ -0,0 +1,75 @@ +/** + * Canonical inventory of the Live overlay's UI surfaces: one entry per piece of + * chrome Live mounts on the user's page, with the element ids that make it up. + * + * Single source of truth, consumed by: + * - skill/scripts/live/browser-script-parts.mjs — serializes this into + * window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude. + * - skill/scripts/live-browser.js — publishes it on + * window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That + * file is served raw and injected as a classic `; } @@ -944,22 +1064,29 @@ const server = http.createServer((req, res) => { let parsed = {}; try { parsed = JSON.parse(body); } catch { /* empty steer */ } const chosen = options.find((o) => o.id === parsed.optionId); + const isReroll = parsed.optionId === 'reroll'; + // A followup round's pick is not terminal: the table stays open for the + // next round (--update), exactly like a re-roll. Detached mode only; + // the blocking mode has no update channel, so its picks stay terminal. + const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll; const answer = JSON.stringify({ optionId: parsed.optionId ?? null, steer: parsed.steer ?? '', + ...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}), + ...(followupOpen ? { followup: true } : {}), ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}), ...(chosen?.sketch ? { sketch: chosen.sketch } : {}), }); - const isReroll = parsed.optionId === 'reroll'; if (detachedKey) { fs.mkdirSync(QUESTION_DIR, { recursive: true }); fs.writeFileSync(answerFile(detachedKey), answer + '\n'); } else { printAnswer(answer); } - // A re-roll in detached mode keeps the table open: the client shows a - // loading hand and reloads when --update delivers the next round. - if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150); + // A re-roll or followup pick in detached mode keeps the table open: the + // client shows a loading hand and reloads when --update delivers the + // next round. + if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150); }); return; } From dbff0880e62a0c05efbc099370a5c89384c0eb9b Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Sat, 8 Aug 2026 15:47:11 -0700 Subject: [PATCH 19/35] Decision page: full-fidelity comps, raise cycler, declined sizing, canon order, full card anatomy (#545) * Polish the decision page: raise cycler, declined height, canon order, full card anatomy Field feedback from the first real rolls of the verdict-routed hand: - Several raises stacked on the assigned card blew it out of proportion. More than one raise now renders as a compact cycler: one visible, a counter, click or Enter advances. A single raise stays inline. - Declined cards inherited the row's stretch alignment, so a narrow card stood at the tallest contender's height, a strange stilt beside the hand. They now size to their content. - Deck order becomes a gradient of standing: contenders, then the canon, then declined dead last. The canon between full alternates and the demoted row reads as the familiar door rather than the last resort after the rejects. - Root cause of bare-bones challenger and canon cards in the field: the --schema example only gave the assigned card palette, materials, and risk, and models author payloads by imitating the example, so the "same anatomy on every card" instruction lost to it every time. The example now carries full anatomy on every card and the schema note says a card with no palette chips is an authoring gap, not a data gap. AI-assisted change. Co-Authored-By: Claude Code * Decision cards carry full-fidelity comps instead of sketches Field verdict on the sketch contract: the sketches came back too simple to inform the choice, and generation takes the same time at any fidelity, so the deliberately-unfinished frame paid comp cost for sketch quality. The decision card's image is now that direction's north-star comp, produced under visualize.md's comp discipline (structure-led prompt, real name and content, no invented commercial claims), saved under .impeccable/mocks/ with its prompt sidecar. Fairness between cards comes from equal fidelity in each card's own grammar rather than shared unfinishedness. The chosen card's comp is never spent by the choice: on a comp-led build it enters the comp round as compositional option one (visualize.md now generates two variations beside it; a round arriving with no decision comp still renders all three), and on a code-led build it returns at the finish review as the critique reference. Produce order still front-loads a re-roll's spend onto the cards read first. serve-question keeps the sketch field's wire name for payload compatibility; docs, schema paths, shimmer labels, and the answer directive (CHOSEN COMP) speak comp. AI-assisted change. Co-Authored-By: Claude Code * fix: address PR review bot findings on the comp round - Producer still forced sketches (cursor, high): the asset producer's Decision Sketches contract still mandated deliberately unfinished matte sketches, so the parallel path would keep shipping sketch-era images. The section is now Decision Comps: full-fidelity north-star comp, structure-led prompt, equal commitment across siblings, no invented claims, sidecar written. - Mocks collided with the approval check (cursor, high): decision comps now live under .impeccable/mocks/decision/, visualize.md scopes the no-approval finding to comp-round output, new-work.md states the unchosen hand implies no approval, and the code-led finish packet names the chosen decision comp as the critique reference in the approved-comp slot. - Raise cycler announces (greptile, both P1s): a visually hidden aria-live region reads out the newly active raise and its position on advance; initial render stays quiet. - Declined width in the vertical deck (cursor, medium): align-self: flex-start shrank declined cards to content width in the portrait column layout, where the cross axis is horizontal; they stretch there and keep content height in the row layout. AI-assisted change. Co-Authored-By: Claude Code * fix: raise cycler tooltip and label name both input modes Copilot: the tooltip said Click while the control also answers Enter and Space; the title and a new aria-label now say activate/press Enter. AI-assisted change. Co-Authored-By: Claude Code * fix: finish reviewer exempts decision comps from the approval check cursor[bot] follow-through: the reviewer's Persistence check still treated any comps under .impeccable/mocks/ as approval-gated, and the reviewer never reads visualize.md by design, so code-led and spent-hand rounds could draw a false skipped-approval finding. The check now scopes to comp-round comps, exempts .impeccable/mocks/decision/ as the direction round's dealt hand, and defines how a code-led build's decision comp is judged in the approved-comp slot: the critique reference, under the no-approved-comp fidelity rules plus what the image dared that the build did not. AI-assisted change. Co-Authored-By: Claude Code * fix: the critique reference is its own reviewer input, not the approved-comp slot cursor[bot]: passing the code-led decision comp through the approved-comp slot dragged in that slot's obligations (inventory-first reading, the fidelity matrix, Truth's shipped-asset demand for every image-native region), which contradicts code-led's premise. The input contract now names it a separate labeled critique-reference input that nothing binding "the approved comp" touches, and Fidelity defines its treatment where the no-approved-comp rules live: provocation, not spec; no matrix, citations, or asset obligations; its dares enter material_fixes as ordinary fixes. AI-assisted change. Co-Authored-By: Claude Code --------- Co-authored-by: Claude Code --- skill/agents/impeccable-asset-producer.md | 4 +- skill/agents/impeccable-finish-reviewer.md | 6 +- skill/reference/new-work.md | 6 +- skill/reference/visualize.md | 4 +- skill/scripts/serve-question.mjs | 96 +++++++++++++++++----- tests/new-work-e2e.test.mjs | 4 +- tests/serve-question.test.mjs | 2 +- 7 files changed, 89 insertions(+), 33 deletions(-) diff --git a/skill/agents/impeccable-asset-producer.md b/skill/agents/impeccable-asset-producer.md index 0808db3b1..6af0ab8ea 100644 --- a/skill/agents/impeccable-asset-producer.md +++ b/skill/agents/impeccable-asset-producer.md @@ -22,9 +22,9 @@ Your job is production cleanup, not new art direction. Work only from the approv Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster. -## Decision Sketches +## Decision Comps -When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame, including its aspect: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. The frame is shared across siblings so no sketch looks more finished than another; a finish gap breaks the comparison. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a sketch run. +When the parent hands you a decision card packet instead of an approved mock, the job is one comp: one card, one file, written to the card's declared `sketch` path (the field keeps its wire name) the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a comp is reported back, not padded from imagination. Render the card's direction as a north-star comp at full fidelity: the requested surface's first viewport, prompt led by the surface's own structure (its regions named in order with their scale relationships, never the world's atmosphere), fully committed in the card's own palette, type character, and material world; a native app or mobile-first surface is a portrait frame at its device viewport, never a landscape default. Every sibling renders at the same full fidelity in its own grammar, one surface, one aspect; equal commitment is what keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Write the prompt sidecar beside the file. Return one line naming the path and any deviation, nothing more. Everything below this section is the asset-production job; none of it applies to a decision-comp run. ## Input Contract diff --git a/skill/agents/impeccable-finish-reviewer.md b/skill/agents/impeccable-finish-reviewer.md index 3964c09e3..588c1545e 100644 --- a/skill/agents/impeccable-finish-reviewer.md +++ b/skill/agents/impeccable-finish-reviewer.md @@ -22,12 +22,12 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path; and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order -1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comps with no recorded pick mean the approval point was skipped, and that is a material finding. -2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. +1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding. When comp-round comps exist under `.impeccable/mocks/`, an approval record exists too, the surface brief naming the approved comp or an `approved` flag in its sidecar; comp-round comps with no recorded pick mean the approval point was skipped, and that is a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and they imply no approval whatever the build path; a code-led build has no comp round at all. +2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, the primary action's treatment (a CTA the comp physically works, dissolves, or stamps is a signature element, and its plain-rectangle rendition is contradicted), navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. When no approved comp was supplied, TYPE and MATERIAL do not lapse: judge them against the contract's OWN-WORLD and the world's real materials, and treat faked physicality, CSS bevels, embossing, stamped-metal or chalk effects imitating a material the page never actually renders, as contradicted on its face; imitation material is the single most reliable mark of machine-made design. A critique-reference comp, when one arrived on such a build, is provocation rather than spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is the question of what the image dared that the build did not, and the dares worth adopting enter material_fixes as ordinary ordered fixes. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. When MATERIAL is contradicted on the focal element, or contradiction is the page rather than the exception, stop ordering repairs: make the first material fix a rebuild directive naming the comp regions to re-derive and the assets to produce; a list of patches against a rejected page launders the rejection into an approval. In every material_fixes list, a fix that requires producing an asset says so explicitly ("produce: as a raster asset"), never phrased as a style adjustment the parent will answer with CSS. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement. 3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition. 4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport. 5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material. diff --git a/skill/reference/new-work.md b/skill/reference/new-work.md index c39e84810..adddcc12e 100644 --- a/skill/reference/new-work.md +++ b/skill/reference/new-work.md @@ -48,7 +48,7 @@ The script assigns which structure gets built; your top-ranked structure is what The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading and its raised lines included, the pick card when one exists, the dealt challengers as alternates carrying their QUALITY BAR cards plus each challenger's verdict and kept line, re-roll with its safer and bolder registers, steer, plus canon enabled, and `followup: true` when the execution-contract round will follow (it does whenever image generation exists and no standing build-path preference is recorded); a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, routes declined challengers to a demoted row on its own, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node {{scripts_path}}/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. -When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The frame's aspect is the surface's own: a native app or mobile-first surface sketches portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen sketched landscape is a broken frame, not a neutral default. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the pick, then the full-card hand, then canon, each file written the moment it is done; declined challengers get no sketch, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. +When image generation exists, every card also declares a `sketch` path under `.impeccable/mocks/decision/` (the field keeps its wire name for compatibility; what it carries is the card's comp), the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the comps; the page shimmer-waits per slot and the user may answer before they land. Each card's image is that direction's north-star comp at full fidelity, produced under the comp discipline in [visualize.md](visualize.md): the requested surface's first viewport, structure-led prompt, real product name and real content, no invented commercial claims, in that card's own palette, type character, and material world, committed all the way. Generation takes the same time at any fidelity, so an unfinished sketch pays sketch quality for comp cost; fairness between cards comes from equal fidelity in each card's own grammar, one surface, one aspect, never from shared unfinishedness. The frame's aspect is the surface's own: a native app or mobile-first surface comps portrait at its device viewport, a desktop web surface landscape, and the decision page adapts to either, so a phone screen comped landscape is a broken frame, not a neutral default. Produce in the order the user reads, the assigned card, then the pick, then the full-card hand, then canon, each file written with its prompt sidecar the moment it is done, so a re-roll's spend front-loads onto the cards read first; declined challengers get no comp, their catalog thumb is their face. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-comp packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. The chosen card's comp is not spent by the choice: on a comp-led build it enters the comp round as compositional option one, and on a code-led build it returns at the finish review as the critique reference, what the image dared that the build did not. The unchosen comps stay in `.impeccable/mocks/decision/` as the round's spent hand; they carry no approval and imply none. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. The moment the direction lands, one more round on the same open table decides the execution contract. The direction payload declares `followup: true`, so the table stays open after the pick; deliver the build-path payload through `--update` immediately. Two text-only cards. **Comp-led**: a first-viewport comp is generated and it is law, the finish review audits the build against it; boldest composition on the table, fix rounds expected, motion at risk; choosing it makes the comp non-optional, no silent skipping. **Code-led**: no comp of this page and no apology for it; the QUALITY BAR boards still calibrate finish, and the ambition moves into the written contract, the FIRST VIEWPORT block plus a named signature interaction and motion grammar, which the finish reviewer audits in behavior; code-led is not a discount on commitment, the direction still lands fully committed in code. Lead with the chosen world's fit: a costume-heavy catalog world leads comp-led, a quiet or conventional direction leads code-led; the lead is a default, never a decision, and the user flips it freely. A standing preference, voiced once, is recorded as a brand commitment in PRODUCT.md and skips this round on later surfaces. Without image generation there is no fork and no round: code-led is the only path, stated in one line rather than asked. Only a detached table (`--start`) stays open for `--update`: a blocking serve or the structured-tool channel runs the build-path round as its own second question instead, and `followup: true` belongs only on a detached round. @@ -84,7 +84,7 @@ If the work establishes durable strategy for a route or artifact, read its exist Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it. -On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options rendered and put before the user for approval. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. +On a comp-led build, whenever any image generation is available, a harness-native tool or the API fallback context.mjs reports, the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation. @@ -109,6 +109,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node {{scripts_path}}/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths, and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node {{scripts_path}}/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/skill/reference/visualize.md b/skill/reference/visualize.md index d6e3764d8..09dc93182 100644 --- a/skill/reference/visualize.md +++ b/skill/reference/visualize.md @@ -6,7 +6,7 @@ The purpose of a probe is to test composition, narrative, hierarchy, density, fo ## Generate three compositional options -Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. A decision-page sketch is not a probe: it chose the direction at deliberately unfinished fidelity, so the three comps render regardless, and the chosen card's sketch seeds at most one of them. +Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. - A comp is a designed surface, not a picture of the subject. Lead the generation prompt with the surface's own structure, whatever regions this design actually has, named in order with their scale relationships; a page with no navigation states that instead of inventing one, and an unconventional surface states its unconventional skeleton. A prompt that leads with the world's atmosphere gets a vignette back: the model paints the fish market instead of the fish market's website. Self-check every render: if it could hang as a poster, or reads as a photograph or scene with some text on it, it is not a comp; regenerate with the layout scaffold stated more literally. - When the user shortlisted multiple concepts, spread the three across them. @@ -22,7 +22,7 @@ Show the three together: in the harness when it can display images, otherwise on Do not begin code until the user approves a direction or explicitly delegates the choice. If they delegate, choose using the task brief, PRODUCT.md, and DESIGN.md, and state the evidence. Approval refines the task concept; it does not modify DESIGN.md. -This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build with generated comps and no recorded approval as carrying a material finding. +This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is still recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats a build whose comp round produced comps with no recorded approval as carrying a material finding; decision comps under `.impeccable/mocks/decision/` are the direction round's hand, not comp-round output, and imply no approval on their own. After approval, record the choice where tools can find it: the approved comp's path goes in the surface brief, and the approved comp's `.json` prompt sidecar gains `"approved": true` (every comp generated through `generate-image.mjs` has one; create it if a native tool didn't). The sidecar travels with the mocks folder, so the approval survives sessions and machines that never see the brief. Then summarize the composition and the parts of the comp that must not be literalized, return to new-work.md, record the direction contract from the approved surface concept, and build. diff --git a/skill/scripts/serve-question.mjs b/skill/scripts/serve-question.mjs index bbbbdf77e..788211920 100644 --- a/skill/scripts/serve-question.mjs +++ b/skill/scripts/serve-question.mjs @@ -42,10 +42,12 @@ * // raise lines under the identity row * "risk": "one line: the honest risk", // optional * "body": "fallback prose when the structured fields are absent", - * "sketch": ".impeccable/sketches/assigned.webp", // optional; may not exist - * // yet: the page shimmer-waits and polls the - * // slot until the file lands, so serve first - * // and generate after + * "sketch": ".impeccable/mocks/decision/assigned.webp", // optional; the card's + * // full-fidelity direction comp (the field + * // keeps the sketch era's wire name). May not + * // exist yet: the page shimmer-waits and + * // polls the slot until the file lands, so + * // serve first and generate after * "hero": "https://... or /abs/path.webp", // optional inspiration image; * // rides picture-in-picture when a sketch exists * "board": "https://... or /abs/path.webp" // optional secondary image @@ -147,7 +149,7 @@ function printAnswer(raw) { console.log("CHOSEN CARD: open the chosen world's board and hero images now, before any code. When your harness only reads files, or runs sandboxed, download them INTO the workspace and open the relative path; a sandboxed viewer rejects absolute paths outside it. They set the craft bar the build must reach."); } if (a.sketch) { - console.log('CHOSEN SKETCH: the decision sketch at that path may seed one comp probe; the comp round still renders its full set, because a sketch chose the direction, not the composition.'); + console.log('CHOSEN COMP: the decision comp at that path is compositional option one. On a comp-led build the comp round adds two variations beside it; on a code-led build it returns at the finish review as the critique reference. Never regenerate it from scratch.'); } if (a.optionId === 'canon') { console.log('CANON CHOSEN: the user picked the category standard on purpose. Ask once for two or three products this should sit alongside; their craft level becomes the quality bar. Execute the canon at full commitment, conventions embraced without irony or smuggled quirk.'); @@ -173,17 +175,17 @@ if (hasFlag('schema')) { title: 'Choose the visual world', question: 'The roll assigned Fillmore Handbill. Keep it, take an alternate, or re-roll.', options: [ - { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, - { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/sketches/model-pick.webp' }, - { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, - { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, + { id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/mocks/decision/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' }, + { id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', palette: ['#1f1c18', '#efe5d0', '#a33327'], materials: ['woodcut', 'rag paper'], viewport: 'One tall sheet, the newest release as today’s ballad.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/mocks/decision/model-pick.webp' }, + { id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', palette: ['#0000c0', '#ffff00', '#00c000', '#ffffff'], materials: ['block mosaic', 'phosphor glow'], viewport: 'P100 index page, releases as numbered rows.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', risk: 'Reads retro-novelty when the grid is not strict.', sketch: '.impeccable/mocks/decision/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, + { id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', palette: ['#101418', '#9fb4c0'], materials: ['film grain', 'backlit glass'], case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' }, ], reroll: { registers: ['safer', 'bolder'] }, canon: true, - canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', viewport: 'The arrangement a visitor expects, at full craft.', sketch: '.impeccable/sketches/canon.webp' }, + canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', palette: ['#ffffff', '#111827', '#2563eb'], materials: ['clean grid', 'product photography'], viewport: 'The arrangement a visitor expects, at full craft.', risk: 'Indistinguishable from the competition by design.', sketch: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the sketch, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no sketch (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "MY PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. Sketch aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); + console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the sketch, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no sketch (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "MY PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. The sketch slot carries the card’s full-fidelity direction comp (the field keeps its wire name for compatibility). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); process.exit(0); } @@ -337,16 +339,19 @@ function loadRound(json) { sketchSrc: sketchSrc(option.sketch), }); options = parsed.options.map(decorate); - // The verdict routes rendering: full cards first, declined cards demoted to - // the end of the deck in their own payload order. The reorder happens here - // so a payload that interleaves them still renders the weighing's shape. + // The verdict routes rendering: full cards first, then the canon, then the + // declined cards dead last in their own payload order. The reorder happens + // here so a payload that interleaves them still renders the weighing's + // shape, and the deck reads as a gradient of standing: contenders, the + // familiar door, then the demoted row. const declined = options.filter((o) => o.verdict === 'declined'); - if (declined.length) options = [...options.filter((o) => o.verdict !== 'declined'), ...declined]; + options = options.filter((o) => o.verdict !== 'declined'); // The standing exit as a full card: same anatomy, reserved id, rendered // subordinate by the page. Without it, canon stays the quiet footer action. if (parsed.canonCard && typeof parsed.canonCard === 'object') { options = [...options, { ...decorate(parsed.canonCard), id: 'canon', isCanon: true }]; } + options = [...options, ...declined]; } try { loadRound(raw); } catch (error) { console.error(`serve-question: ${error.message}`); process.exit(1); } const detachedKey = hasFlag('detached-serve') ? arg('key') : null; @@ -392,10 +397,21 @@ function page() { if (idBits.length) rows.push(`
${idBits.join('')}
`); // Donations from declined challengers render as named raise lines: the // assigned card arrives already raised by the hand it beat, and the raise - // is readable, because a raise nobody can read did not happen. + // is readable, because a raise nobody can read did not happen. One raise + // renders inline; several become a compact cycler (click advances), so a + // generous hand cannot blow the card out of proportion. if (Array.isArray(option.raised) && option.raised.length) { const nameOf = (id) => options.find((o) => o.id === id)?.label || String(id ?? ''); - rows.push(`
${option.raised.slice(0, 4).map((r) => `

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

hello

\n'); + + // Detect exits 0 on a clean scan and 2 when findings surface; either way it + // must be the detector answering, not the init redirect. --json makes that + // positive: the detector always prints a JSON findings array. + let output = ''; + try { + output = run('init --json', { cwd: tmp, stdio: 'pipe' }); + } catch (e) { + output = `${e.stdout || ''}${e.stderr || ''}`; + } + + expect(output.trim().startsWith('[')).toBe(true); + expect(output).not.toContain('is not a CLI command'); + expect(output).not.toContain('Unknown command'); + + rmSync(tmp, { recursive: true, force: true }); + }, 60000); + test('formats detected harnesses as concise source-to-target rows', () => { const tmp = mkdtempSync(join(tmpdir(), 'imp-test-detect-lines-')); const home = mkdtempSync(join(tmpdir(), 'imp-home-detect-lines-')); From ddf4526fb504574606b08b98dcfcbe109258f7b0 Mon Sep 17 00:00:00 2001 From: Abdul Wahab <32850166+abdulwahabone@users.noreply.github.com> Date: Sun, 9 Aug 2026 06:41:19 +0500 Subject: [PATCH 25/35] Fix Windows libuv abort in concept-seed after a successful roll (#526) * Fix Windows libuv abort in concept-seed after a successful roll process.exit() with a live fetch keep-alive socket trips libuv's UV_HANDLE_CLOSING assertion on Windows (nodejs/node#56645), aborting the CLI with 0xC0000409 after complete output on the successful-roll path. Destroy the global undici dispatcher before the explicit exit so no socket is left to race; the hard exit stays, keeping the no-linger guarantee on blackholed networks. Fixes #504 Prepared with AI assistance (Cursor agent) under maintainer direction. * Add regression test for the successful-API dispatcher teardown The suite covered local rolls and the unreachable-API fallback but never a successful roll, the one path where a pooled keep-alive socket exists at exit (issue #504). Serve a real /api/roll from a local server and assert the CLI destroys fetch's global dispatcher before its explicit exit. Verified to fail without the fix. Prepared with AI assistance (Cursor agent) under maintainer direction. Co-authored-by: Cursor --------- Co-authored-by: Cursor --- skill/scripts/concept-seed.mjs | 9 +++- tests/concept-seed.test.mjs | 98 +++++++++++++++++++++++++++++++++- 2 files changed, 104 insertions(+), 3 deletions(-) diff --git a/skill/scripts/concept-seed.mjs b/skill/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/skill/scripts/concept-seed.mjs +++ b/skill/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/tests/concept-seed.test.mjs b/tests/concept-seed.test.mjs index e1963f84c..2991d57e2 100644 --- a/tests/concept-seed.test.mjs +++ b/tests/concept-seed.test.mjs @@ -1,10 +1,11 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; -import { spawnSync } from 'node:child_process'; +import { spawn, spawnSync } from 'node:child_process'; import { mkdtempSync, writeFileSync } from 'node:fs'; +import { createServer } from 'node:http'; import { tmpdir } from 'node:os'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import { readConceptCatalog, validateConceptCatalog, @@ -711,3 +712,96 @@ describe('init gate', () => { ); }); }); + +// The Windows abort in issue #504 (nodejs/node#56645) needs three things at +// once: a successful roll over Node's undici-backed fetch, the keep-alive +// socket that success leaves pooled, and the explicit process.exit at the end +// of the CLI. The suite's other API test exercises only the unreachable-API +// fallback, which leaves no pooled socket and so never walked the crashing +// path. This one serves a real roll from a local server and asserts the CLI +// destroys fetch's global dispatcher before exiting, so the teardown cannot +// silently regress. The teardown is Node fetch internals, so the CLI is +// spawned with node even when the suite itself runs under bun. +describe('API roll path', () => { + const NODE = process.versions.bun ? 'node' : process.execPath; + + const ROLL_PAYLOAD = { + poolRevision: 'api-test-rev', + approvedCount: 6, + catalogCount: 9, + challengers: [{ + id: 'api-test-world', + form: 'a letterpress print shop, where type, ink, and impression organize the page', + spark: 'Deep impressions hold the central promise while loose sorts wait in the case.', + system: ['Palette/material: dense ink black bitten into soft cotton paper'], + webLeverage: 'Variable-font impression depth with a keyboard-readable page structure', + }], + compositions: [], + }; + + // Wraps the global dispatcher's destroy so the parent test can observe the + // CLI's exit teardown. The warmup fetch makes fetch install the dispatcher + // before the wrap, and parks a keep-alive socket in its pool, which is the + // exact state the Windows crash needs at exit. + const PRELOAD = [ + "const KEY = Symbol.for('undici.globalDispatcher.1');", + 'await fetch(`${process.env.IMPECCABLE_API_URL}/warmup`).then(r => r.arrayBuffer()).catch(() => {});', + 'const dispatcher = globalThis[KEY];', + "if (dispatcher && typeof dispatcher.destroy === 'function') {", + ' const destroy = dispatcher.destroy.bind(dispatcher);', + ' dispatcher.destroy = (...args) => {', + " process.stderr.write('DISPATCHER_DESTROY_CALLED\\n');", + ' return destroy(...args);', + ' };', + '}', + '', + ].join('\n'); + + it('resolves a successful roll and destroys the fetch dispatcher before the explicit exit', async () => { + const requests = []; + const server = createServer((req, res) => { + requests.push(req.url); + if (req.url.startsWith('/api/roll?')) { + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(ROLL_PAYLOAD)); + return; + } + res.statusCode = 404; + res.end('not found'); + }); + await new Promise(resolveListen => server.listen(0, '127.0.0.1', resolveListen)); + try { + const dir = mkdtempSync(path.join(tmpdir(), 'concept-seed-api-')); + writeFileSync(path.join(dir, 'PRODUCT.md'), '# Test Product\n\n## Platform\n\nweb\n'); + const preloadPath = path.join(dir, 'wrap-dispatcher.mjs'); + writeFileSync(preloadPath, PRELOAD); + const result = await new Promise((resolveRun, rejectRun) => { + const child = spawn(NODE, [ + '--import', pathToFileURL(preloadPath).href, + SCRIPT, '--scope', 'direction', '--mode', 'persuade', '--from', 'api-test', + ], { + cwd: dir, + env: { + ...process.env, + IMPECCABLE_CATALOG_DIR: '/nonexistent-catalog-dir', + IMPECCABLE_API_URL: `http://127.0.0.1:${server.address().port}/api`, + }, + }); + let stdout = ''; + let stderr = ''; + child.stdout.on('data', chunk => { stdout += chunk; }); + child.stderr.on('data', chunk => { stderr += chunk; }); + child.on('error', rejectRun); + child.on('close', status => resolveRun({ status, stdout, stderr })); + }); + assert.equal(result.status, 0, `stderr: ${result.stderr}`); + assert.equal(requests.some(url => url.startsWith('/api/roll?')), true, 'the CLI must hit the roll endpoint'); + assert.match(result.stdout, /source: api/); + assert.match(result.stdout, /letterpress print shop/); + assert.match(result.stdout, /TELEMETRY:/); + assert.match(result.stderr, /DISPATCHER_DESTROY_CALLED/, 'the dispatcher must be destroyed before process.exit'); + } finally { + server.close(); + } + }); +}); From 4596f3183cf1ae32aa527e40b3d558f021a49bb1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 9 Aug 2026 01:41:51 +0000 Subject: [PATCH 26/35] Sync generated provider output --- .agents/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .claude/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .cursor/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .gemini/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .github/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .grok/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .kiro/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .opencode/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .pi/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .qoder/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .rovodev/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .trae-cn/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .trae/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- .vibe/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- plugin/skills/impeccable/scripts/concept-seed.mjs | 9 ++++++++- 15 files changed, 120 insertions(+), 15 deletions(-) diff --git a/.agents/skills/impeccable/scripts/concept-seed.mjs b/.agents/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.agents/skills/impeccable/scripts/concept-seed.mjs +++ b/.agents/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.claude/skills/impeccable/scripts/concept-seed.mjs b/.claude/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.claude/skills/impeccable/scripts/concept-seed.mjs +++ b/.claude/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.cursor/skills/impeccable/scripts/concept-seed.mjs b/.cursor/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.cursor/skills/impeccable/scripts/concept-seed.mjs +++ b/.cursor/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.gemini/skills/impeccable/scripts/concept-seed.mjs b/.gemini/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.gemini/skills/impeccable/scripts/concept-seed.mjs +++ b/.gemini/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.github/skills/impeccable/scripts/concept-seed.mjs b/.github/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.github/skills/impeccable/scripts/concept-seed.mjs +++ b/.github/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.grok/skills/impeccable/scripts/concept-seed.mjs b/.grok/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.grok/skills/impeccable/scripts/concept-seed.mjs +++ b/.grok/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.kiro/skills/impeccable/scripts/concept-seed.mjs b/.kiro/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.kiro/skills/impeccable/scripts/concept-seed.mjs +++ b/.kiro/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.opencode/skills/impeccable/scripts/concept-seed.mjs b/.opencode/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.opencode/skills/impeccable/scripts/concept-seed.mjs +++ b/.opencode/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.pi/skills/impeccable/scripts/concept-seed.mjs b/.pi/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.pi/skills/impeccable/scripts/concept-seed.mjs +++ b/.pi/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.qoder/skills/impeccable/scripts/concept-seed.mjs b/.qoder/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.qoder/skills/impeccable/scripts/concept-seed.mjs +++ b/.qoder/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.rovodev/skills/impeccable/scripts/concept-seed.mjs b/.rovodev/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.rovodev/skills/impeccable/scripts/concept-seed.mjs +++ b/.rovodev/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.trae-cn/skills/impeccable/scripts/concept-seed.mjs b/.trae-cn/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.trae-cn/skills/impeccable/scripts/concept-seed.mjs +++ b/.trae-cn/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.trae/skills/impeccable/scripts/concept-seed.mjs b/.trae/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.trae/skills/impeccable/scripts/concept-seed.mjs +++ b/.trae/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/.vibe/skills/impeccable/scripts/concept-seed.mjs b/.vibe/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/.vibe/skills/impeccable/scripts/concept-seed.mjs +++ b/.vibe/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } diff --git a/plugin/skills/impeccable/scripts/concept-seed.mjs b/plugin/skills/impeccable/scripts/concept-seed.mjs index 5b4345818..db638ab57 100644 --- a/plugin/skills/impeccable/scripts/concept-seed.mjs +++ b/plugin/skills/impeccable/scripts/concept-seed.mjs @@ -692,6 +692,13 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur process.exitCode = 1; } // A raced-out fetch may still hold a socket; exit explicitly so the CLI - // never lingers on a dead network path after output is written. + // never lingers on a dead network path after output is written. Destroy + // fetch's global undici dispatcher first: process.exit() with a live + // keep-alive socket trips a libuv assertion on Windows and aborts the + // process after a successful roll (nodejs/node#56645). + const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')]; + if (dispatcher && typeof dispatcher.destroy === 'function') { + try { await dispatcher.destroy(); } catch { /* exit regardless */ } + } process.exit(process.exitCode ?? 0); } From 490dcfd6783b25b2068bc7d95e7687e46099816a Mon Sep 17 00:00:00 2001 From: Abdul Wahab <32850166+abdulwahabone@users.noreply.github.com> Date: Sun, 9 Aug 2026 06:43:17 +0500 Subject: [PATCH 27/35] Fix #476: stop using JSON.stringify/double quotes as shell quoting in four exec sites (#533) * Fix: use argv exec and single-quote escaping for the four #476 shell-injection sites JSON.stringify and raw double-quote interpolation were used as shell quoting, but /bin/sh still expands $(...), backticks, and ${} inside double quotes. - is-generated.mjs / live.mjs runScript: switch execSync string commands to execFileSync argv form, which never invokes a shell. Closes the remote path where a source file named `$(...)` executes during the live-mode walk. - skills.mjs hook command + hook-lib.mjs ignore-value suggestion: values that must stay shell strings now use POSIX single-quote escaping instead of JSON/double quotes. The doctor's hook-token parser learns the single-quoted absolute form so it keeps verifying user-level installs. Adds regression tests for the single-quoted absolute hook form and the single-quoted ignore-value suggestion. Verified end to end in a browser through a real live-mode wrap walk against a hostile-named source file. Prepared with AI assistance (Cursor) under maintainer instruction. Co-authored-by: Cursor * Test: lock in POSIX single-quoting for a $(...) absolute install path (#476) Follow-up from security review: prove an install path embedding $(...) is single-quoted in the written hook manifest, not double-quoted. Prepared with AI assistance (Cursor) under maintainer instruction. Co-authored-by: Cursor * Fix: quote ignore-command args per platform so Windows cmd.exe keeps spaces (#533) Greptile flagged that switching quoteCommandArg to POSIX single quotes fixed $(...) injection on /bin/sh but regressed Windows cmd.exe, where single quotes are literal, so a --file path containing spaces was split and the ignore scope was stored malformed. The suggested command runs on the same machine the hook fired on, so branch on process.platform (the pattern skills.mjs already uses): single-quote on POSIX for the #476 fix, and keep the original double-quote escaping on Windows so that path's behavior is unchanged. Adds a regression test asserting both forms. Prepared with AI assistance (Cursor) under maintainer instruction. Co-authored-by: Cursor * Test: prove the POSIX hook guard is inert under /bin/sh and Windows keeps double quotes (#533) Greptile's probe could not reach the generated manifest, leaving the hook command contract unverified. Convert that into committed proof: - POSIX: install with a $(touch pwned) absolute path, then actually execute the generated guard under /bin/sh from a clean cwd and assert no marker file appears and the guard exits 0 (single-quoted substitution stays inert). - Windows: drive copyProviderHooks as win32 in-process and assert the command keeps the double-quoted absolute path (usable when the install path has spaces; $(...) is inert on cmd.exe anyway). Test-only; source quoting is unchanged. Prepared with AI assistance (Cursor) under maintainer instruction. Co-authored-by: Cursor --------- Co-authored-by: Cursor --- cli/bin/commands/skills.mjs | 26 ++++++++--- skill/scripts/hook-lib.mjs | 14 +++++- skill/scripts/lib/is-generated.mjs | 7 ++- skill/scripts/lib/staleness-deep.mjs | 9 +++- skill/scripts/live.mjs | 14 ++++-- tests/doctor.test.mjs | 23 ++++++++++ tests/hook.test.mjs | 36 +++++++++++++++ tests/skills-cli.test.js | 67 +++++++++++++++++++++++++++- 8 files changed, 182 insertions(+), 14 deletions(-) diff --git a/cli/bin/commands/skills.mjs b/cli/bin/commands/skills.mjs index 03ce12454..c57c4d0a6 100644 --- a/cli/bin/commands/skills.mjs +++ b/cli/bin/commands/skills.mjs @@ -1365,17 +1365,28 @@ function hookScriptPathForProvider(skillRoot, provider) { // with single quotes for the inner string literals. const WIN32_HOOK_GUARD_SCRIPT = "const p=process.argv[1];const f=require('fs');if(f.existsSync(p)){const r=require('child_process').spawnSync(process.execPath,[p],{stdio:'inherit'});process.exit(r.status===null?1:r.status);}"; +// POSIX single-quote escaping. JSON.stringify is not shell quoting: inside +// double quotes /bin/sh still expands $(...), backticks, and ${}, and this +// string is baked into a hook manifest the harness re-executes on every edit, +// so an install path embedding $(...) would run it repeatedly (issue #476). +// Windows command forms keep double quotes: cmd.exe treats ' as a literal +// character and performs no command substitution. +function shSingleQuote(value) { + return `'${String(value).replace(/'/g, `'\\''`)}'`; +} + function windowsHookCommand(quotedPath) { return `if exist ${quotedPath} (node ${quotedPath} & exit /b)`; } +// `quotedPath` carries one pre-quoted form per target shell: { posix, win32 }. function guardHookCommand(quotedPath, provider) { // `.agents` (Codex) keeps the POSIX form unconditionally: its Windows // consumers read the commandWindows sibling instead. if (provider !== '.agents' && process.platform === 'win32') { - return `node -e "${WIN32_HOOK_GUARD_SCRIPT}" ${quotedPath}`; + return `node -e "${WIN32_HOOK_GUARD_SCRIPT}" ${quotedPath.win32}`; } - return `[ ! -f ${quotedPath} ] || node ${quotedPath}`; + return `[ ! -f ${quotedPath.posix} ] || node ${quotedPath.posix}`; } // Transform bundled hook commands for the actual install target: @@ -1398,9 +1409,14 @@ function rewriteHookCommandsForSkillRoot(value, provider, { skillRoot, absolute // Project-scope installs derive the provider's own project-relative path // rather than trusting the bundle token, which for Codex points at // `.codex/skills/...` while the CLI installs the skill at `.agents/skills/`. + // The absolute path comes from the install root (project dir or $HOME), so + // its POSIX form gets real single-quote escaping (issue #476). The relative + // form is a per-provider constant and stays double-quoted, because Claude's + // ${CLAUDE_PROJECT_DIR} token must keep expanding at hook time. + const relPath = hookScriptRelPathForProvider(provider); const quotedPath = absolute - ? JSON.stringify(hookScript) - : JSON.stringify(hookScriptRelPathForProvider(provider)); + ? { posix: shSingleQuote(hookScript), win32: JSON.stringify(hookScript) } + : { posix: JSON.stringify(relPath), win32: JSON.stringify(relPath) }; if (typeof value === 'string') { if (!valueHasImpeccableHookMarker(value)) return value; @@ -1415,7 +1431,7 @@ function rewriteHookCommandsForSkillRoot(value, provider, { skillRoot, absolute next[key] = rewriteHookCommandsForSkillRoot(child, provider, { skillRoot, absolute }); } if (provider === '.agents' && typeof value.command === 'string' && valueHasImpeccableHookMarker(value.command)) { - next.commandWindows = windowsHookCommand(quotedPath); + next.commandWindows = windowsHookCommand(quotedPath.win32); } return next; } diff --git a/skill/scripts/hook-lib.mjs b/skill/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/skill/scripts/hook-lib.mjs +++ b/skill/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/skill/scripts/lib/is-generated.mjs b/skill/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/skill/scripts/lib/is-generated.mjs +++ b/skill/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/skill/scripts/lib/staleness-deep.mjs b/skill/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/skill/scripts/lib/staleness-deep.mjs +++ b/skill/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/skill/scripts/live.mjs b/skill/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/skill/scripts/live.mjs +++ b/skill/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/tests/doctor.test.mjs b/tests/doctor.test.mjs index 7d801dfb8..877c38b0e 100644 --- a/tests/doctor.test.mjs +++ b/tests/doctor.test.mjs @@ -486,6 +486,29 @@ describe('checkHookInstallation', () => { ); }); + it('handles the #476 single-quoted absolute form (user-level installs)', () => { + // The shell-injection fix single-quotes the absolute POSIX path instead of + // JSON.stringify. The doctor's token parser must read the single-quoted + // form too, or it silently stops verifying every user-level install. + const abs = path.join(scratch, '.claude', 'skills', 'impeccable', 'scripts', 'hook.mjs'); + const p = `'${abs}'`; + const guarded = `[ ! -f ${p} ] || node ${p}`; + write('.claude/settings.json', JSON.stringify({ + hooks: { Stop: [{ hooks: [{ command: guarded }] }] }, + })); + // absolute path missing → flagged + assert.deepEqual( + ids(checkHookInstallation({ projectRoot: scratch, repoRoot: scratch, providerId: 'claude-code' })), + ['hook-script-missing'], + ); + // present → quiet + write('.claude/skills/impeccable/scripts/hook.mjs', '// hook\n'); + assert.deepEqual( + checkHookInstallation({ projectRoot: scratch, repoRoot: scratch, providerId: 'claude-code' }), + [], + ); + }); + it('never reports missing for the GitHub $(git rev-parse) form', () => { // Command substitution is not statically resolvable; a doctor must not // assert a negative it cannot verify. diff --git a/tests/hook.test.mjs b/tests/hook.test.mjs index 631fc245d..d4c9589db 100644 --- a/tests/hook.test.mjs +++ b/tests/hook.test.mjs @@ -1142,6 +1142,42 @@ describe('renderTemplate()', () => { assert.match(text, /\/impeccable hooks ignore-value bounce-easing bounce-ball --shared/); }); + it('single-quotes a hostile font value so the suggestion cannot inject a shell command (#476)', () => { + // The suggested command comes straight from scanned file content. A + // double-quoted arg would leave $(...) live for whoever runs the + // suggestion; single quotes neutralize it. + const text = renderTemplate( + [finding('overused-font', 1, { + name: 'Overused font', + snippet: 'body { font-family: "$(touch pwned)", sans-serif; }', + })], + '/x/fonts.css', DEFAULT_CONFIG, { cwd: '/x' } + ); + assert.match(text, /ignore-value overused-font '\$\(touch pwned\)' --shared/); + assert.doesNotMatch(text, /ignore-value overused-font "\$\(touch pwned\)"/); + }); + + it('quotes the --file path per platform: single quotes on POSIX, double quotes on Windows (#533)', () => { + // The suggested command is run on the same machine the hook fired on. + // POSIX needs single quotes so $(...) in a filename cannot execute; Windows + // cmd.exe treats single quotes as literal, so a path with spaces must stay + // double-quoted or the ignore scope is split at the space. + const original = process.platform; + const renderFor = (platform) => { + Object.defineProperty(process, 'platform', { value: platform, configurable: true }); + try { + return renderTemplate( + [finding('side-tab', 1, { name: 'Side tab' })], + '/x/My Components/Card.tsx', DEFAULT_CONFIG, { cwd: '/x' } + ); + } finally { + Object.defineProperty(process, 'platform', { value: original, configurable: true }); + } + }; + assert.match(renderFor('linux'), /--file 'My Components\/Card\.tsx'/); + assert.match(renderFor('win32'), /--file "My Components\/Card\.tsx"/); + }); + it('drops the L prefix when line is 0', () => { const text = renderTemplate( [finding('side-tab', 0, { name: 'X' })], diff --git a/tests/skills-cli.test.js b/tests/skills-cli.test.js index c51ef7680..427f33e25 100644 --- a/tests/skills-cli.test.js +++ b/tests/skills-cli.test.js @@ -10,7 +10,7 @@ * gracefully when impeccable.style is unreachable. */ import { describe, test, expect, beforeAll, afterAll } from 'bun:test'; -import { execSync } from 'child_process'; +import { execSync, execFileSync } from 'child_process'; import { mkdtempSync, existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, rmSync, lstatSync, realpathSync, readlinkSync, symlinkSync } from 'fs'; import { join } from 'path'; import { tmpdir } from 'os'; @@ -1728,6 +1728,71 @@ describe('copyProviderHooks: hook command path resolution (#399)', () => { rmSync(tmp, { recursive: true, force: true }); rmSync(skillHome, { recursive: true, force: true }); }); + + test('single-quotes an absolute install path that embeds $(...), and the guard is inert under /bin/sh (#476)', () => { + // A hook command is re-executed by the harness on every edit. JSON.stringify + // is not shell quoting: an install path containing $(...) inside double + // quotes would run on each fire. The absolute POSIX form must be + // single-quoted so the substitution stays inert. + const tmp = mkdtempSync(join(tmpdir(), 'imp-hook-split-')); + const skillHome = mkdtempSync(join(tmpdir(), 'imp-hook-$(touch pwned)-')); + const bundleDir = createProjectDirBundle(tmp); + + copyProviderHooks(bundleDir, tmp, ['.claude'], { skillRoot: skillHome }); + + const raw = readFileSync(join(tmp, '.claude', 'settings.local.json'), 'utf8'); + // The path appears single-quoted, never double-quoted (which would leave + // the substitution live for /bin/sh). + expect(raw).toContain(`'${skillHome}`); + expect(raw).not.toContain(`"${skillHome}`); + + const commands = claudeHookCommands(join(tmp, '.claude', 'settings.local.json')); + expect(commands.length).toBeGreaterThan(0); + // End-to-end: actually run each generated guard under /bin/sh from a clean + // cwd. The hook script does not exist (skillHome is empty), so `[ ! -f ... ]` + // short-circuits and node never runs — and crucially the single-quoted + // $(touch pwned) must not execute. Prove it: no `pwned` file appears and the + // guard exits 0. + if (process.platform !== 'win32') { + const runCwd = mkdtempSync(join(tmpdir(), 'imp-hook-run-')); + for (const command of commands) { + expect(command).toContain('[ ! -f '); + expect(command).not.toMatch(/"[^"]*\$\(touch pwned\)/); + execFileSync('/bin/sh', ['-c', command], { cwd: runCwd, stdio: 'ignore' }); + } + expect(existsSync(join(runCwd, 'pwned'))).toBe(false); + rmSync(runCwd, { recursive: true, force: true }); + } + rmSync(tmp, { recursive: true, force: true }); + rmSync(skillHome, { recursive: true, force: true }); + }); + + test('the Windows hook form keeps a usable double-quoted absolute path (#533)', () => { + // cmd.exe does no $(...) substitution but treats single quotes as literal, + // so the Windows command form must keep the absolute path double-quoted or + // a space in the install path would split the argument. copyProviderHooks + // branches on process.platform, so drive it as win32 in-process. + const original = process.platform; + const tmp = mkdtempSync(join(tmpdir(), 'imp-hook-win-')); + const skillHome = mkdtempSync(join(tmpdir(), 'imp-hook-win-home-')); + const bundleDir = createProjectDirBundle(tmp); + try { + Object.defineProperty(process, 'platform', { value: 'win32', configurable: true }); + copyProviderHooks(bundleDir, tmp, ['.claude'], { skillRoot: skillHome }); + } finally { + Object.defineProperty(process, 'platform', { value: original, configurable: true }); + } + + const absolute = join(skillHome, '.claude', 'skills', 'impeccable', 'scripts', 'hook.mjs'); + for (const command of claudeHookCommands(join(tmp, '.claude', 'settings.local.json'))) { + // Windows guard shape (node -e wrapper) with the absolute path double-quoted. + expect(command).toContain(`"${absolute}"`); + expect(command).not.toContain(`'${absolute}`); + expect(command).toContain('node -e'); + } + rmSync(tmp, { recursive: true, force: true }); + rmSync(skillHome, { recursive: true, force: true }); + }); }); // ─── Update scope resolution (issue #399, part 2) ──────────────────────────── From 5c8652b01994d12caadc6e4416496c29b669b3a9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 9 Aug 2026 01:43:56 +0000 Subject: [PATCH 28/35] Sync generated provider output --- .agents/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .agents/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .claude/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .claude/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .cursor/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .cursor/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .gemini/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .gemini/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .github/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .github/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .grok/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .grok/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .kiro/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .kiro/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .opencode/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .opencode/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .pi/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .pi/skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .pi/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .qoder/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .qoder/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .rovodev/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .rovodev/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .trae-cn/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .trae-cn/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .trae/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .trae/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- .vibe/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- .vibe/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- plugin/skills/impeccable/scripts/hook-lib.mjs | 14 +++++++++++++- .../skills/impeccable/scripts/lib/is-generated.mjs | 7 +++++-- .../impeccable/scripts/lib/staleness-deep.mjs | 9 ++++++++- plugin/skills/impeccable/scripts/live.mjs | 14 ++++++++++---- 60 files changed, 540 insertions(+), 120 deletions(-) diff --git a/.agents/skills/impeccable/scripts/hook-lib.mjs b/.agents/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.agents/skills/impeccable/scripts/hook-lib.mjs +++ b/.agents/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.agents/skills/impeccable/scripts/lib/is-generated.mjs b/.agents/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.agents/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.agents/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.agents/skills/impeccable/scripts/lib/staleness-deep.mjs b/.agents/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.agents/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.agents/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.agents/skills/impeccable/scripts/live.mjs b/.agents/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.agents/skills/impeccable/scripts/live.mjs +++ b/.agents/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.claude/skills/impeccable/scripts/hook-lib.mjs b/.claude/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.claude/skills/impeccable/scripts/hook-lib.mjs +++ b/.claude/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.claude/skills/impeccable/scripts/lib/is-generated.mjs b/.claude/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.claude/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.claude/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.claude/skills/impeccable/scripts/lib/staleness-deep.mjs b/.claude/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.claude/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.claude/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.claude/skills/impeccable/scripts/live.mjs b/.claude/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.claude/skills/impeccable/scripts/live.mjs +++ b/.claude/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.cursor/skills/impeccable/scripts/hook-lib.mjs b/.cursor/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.cursor/skills/impeccable/scripts/hook-lib.mjs +++ b/.cursor/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.cursor/skills/impeccable/scripts/lib/is-generated.mjs b/.cursor/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.cursor/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.cursor/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.cursor/skills/impeccable/scripts/lib/staleness-deep.mjs b/.cursor/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.cursor/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.cursor/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.cursor/skills/impeccable/scripts/live.mjs b/.cursor/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.cursor/skills/impeccable/scripts/live.mjs +++ b/.cursor/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.gemini/skills/impeccable/scripts/hook-lib.mjs b/.gemini/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.gemini/skills/impeccable/scripts/hook-lib.mjs +++ b/.gemini/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.gemini/skills/impeccable/scripts/lib/is-generated.mjs b/.gemini/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.gemini/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.gemini/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.gemini/skills/impeccable/scripts/lib/staleness-deep.mjs b/.gemini/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.gemini/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.gemini/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.gemini/skills/impeccable/scripts/live.mjs b/.gemini/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.gemini/skills/impeccable/scripts/live.mjs +++ b/.gemini/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.github/skills/impeccable/scripts/hook-lib.mjs b/.github/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.github/skills/impeccable/scripts/hook-lib.mjs +++ b/.github/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.github/skills/impeccable/scripts/lib/is-generated.mjs b/.github/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.github/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.github/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.github/skills/impeccable/scripts/lib/staleness-deep.mjs b/.github/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.github/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.github/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.github/skills/impeccable/scripts/live.mjs b/.github/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.github/skills/impeccable/scripts/live.mjs +++ b/.github/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.grok/skills/impeccable/scripts/hook-lib.mjs b/.grok/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.grok/skills/impeccable/scripts/hook-lib.mjs +++ b/.grok/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.grok/skills/impeccable/scripts/lib/is-generated.mjs b/.grok/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.grok/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.grok/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.grok/skills/impeccable/scripts/lib/staleness-deep.mjs b/.grok/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.grok/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.grok/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.grok/skills/impeccable/scripts/live.mjs b/.grok/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.grok/skills/impeccable/scripts/live.mjs +++ b/.grok/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.kiro/skills/impeccable/scripts/hook-lib.mjs b/.kiro/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.kiro/skills/impeccable/scripts/hook-lib.mjs +++ b/.kiro/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.kiro/skills/impeccable/scripts/lib/is-generated.mjs b/.kiro/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.kiro/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.kiro/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.kiro/skills/impeccable/scripts/lib/staleness-deep.mjs b/.kiro/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.kiro/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.kiro/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.kiro/skills/impeccable/scripts/live.mjs b/.kiro/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.kiro/skills/impeccable/scripts/live.mjs +++ b/.kiro/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.opencode/skills/impeccable/scripts/hook-lib.mjs b/.opencode/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.opencode/skills/impeccable/scripts/hook-lib.mjs +++ b/.opencode/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.opencode/skills/impeccable/scripts/lib/is-generated.mjs b/.opencode/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.opencode/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.opencode/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.opencode/skills/impeccable/scripts/lib/staleness-deep.mjs b/.opencode/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.opencode/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.opencode/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.opencode/skills/impeccable/scripts/live.mjs b/.opencode/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.opencode/skills/impeccable/scripts/live.mjs +++ b/.opencode/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.pi/skills/impeccable/scripts/hook-lib.mjs b/.pi/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.pi/skills/impeccable/scripts/hook-lib.mjs +++ b/.pi/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.pi/skills/impeccable/scripts/lib/is-generated.mjs b/.pi/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.pi/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.pi/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.pi/skills/impeccable/scripts/lib/staleness-deep.mjs b/.pi/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.pi/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.pi/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.pi/skills/impeccable/scripts/live.mjs b/.pi/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.pi/skills/impeccable/scripts/live.mjs +++ b/.pi/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.qoder/skills/impeccable/scripts/hook-lib.mjs b/.qoder/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.qoder/skills/impeccable/scripts/hook-lib.mjs +++ b/.qoder/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.qoder/skills/impeccable/scripts/lib/is-generated.mjs b/.qoder/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.qoder/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.qoder/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.qoder/skills/impeccable/scripts/lib/staleness-deep.mjs b/.qoder/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.qoder/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.qoder/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.qoder/skills/impeccable/scripts/live.mjs b/.qoder/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.qoder/skills/impeccable/scripts/live.mjs +++ b/.qoder/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.rovodev/skills/impeccable/scripts/hook-lib.mjs b/.rovodev/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.rovodev/skills/impeccable/scripts/hook-lib.mjs +++ b/.rovodev/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.rovodev/skills/impeccable/scripts/lib/is-generated.mjs b/.rovodev/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.rovodev/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.rovodev/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.rovodev/skills/impeccable/scripts/lib/staleness-deep.mjs b/.rovodev/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.rovodev/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.rovodev/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.rovodev/skills/impeccable/scripts/live.mjs b/.rovodev/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.rovodev/skills/impeccable/scripts/live.mjs +++ b/.rovodev/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.trae-cn/skills/impeccable/scripts/hook-lib.mjs b/.trae-cn/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.trae-cn/skills/impeccable/scripts/hook-lib.mjs +++ b/.trae-cn/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.trae-cn/skills/impeccable/scripts/lib/is-generated.mjs b/.trae-cn/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.trae-cn/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.trae-cn/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.trae-cn/skills/impeccable/scripts/lib/staleness-deep.mjs b/.trae-cn/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.trae-cn/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.trae-cn/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.trae-cn/skills/impeccable/scripts/live.mjs b/.trae-cn/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.trae-cn/skills/impeccable/scripts/live.mjs +++ b/.trae-cn/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.trae/skills/impeccable/scripts/hook-lib.mjs b/.trae/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.trae/skills/impeccable/scripts/hook-lib.mjs +++ b/.trae/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.trae/skills/impeccable/scripts/lib/is-generated.mjs b/.trae/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.trae/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.trae/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.trae/skills/impeccable/scripts/lib/staleness-deep.mjs b/.trae/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.trae/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.trae/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.trae/skills/impeccable/scripts/live.mjs b/.trae/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.trae/skills/impeccable/scripts/live.mjs +++ b/.trae/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/.vibe/skills/impeccable/scripts/hook-lib.mjs b/.vibe/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/.vibe/skills/impeccable/scripts/hook-lib.mjs +++ b/.vibe/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/.vibe/skills/impeccable/scripts/lib/is-generated.mjs b/.vibe/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/.vibe/skills/impeccable/scripts/lib/is-generated.mjs +++ b/.vibe/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/.vibe/skills/impeccable/scripts/lib/staleness-deep.mjs b/.vibe/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/.vibe/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/.vibe/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/.vibe/skills/impeccable/scripts/live.mjs b/.vibe/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/.vibe/skills/impeccable/scripts/live.mjs +++ b/.vibe/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } diff --git a/plugin/skills/impeccable/scripts/hook-lib.mjs b/plugin/skills/impeccable/scripts/hook-lib.mjs index b874985a6..9170aa696 100644 --- a/plugin/skills/impeccable/scripts/hook-lib.mjs +++ b/plugin/skills/impeccable/scripts/hook-lib.mjs @@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) { function quoteCommandArg(value) { const text = String(value || '').trim(); if (/^[A-Za-z0-9._:-]+$/.test(text)) return text; - return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + // The suggestion is meant to be run on this same machine, so quote for its + // shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside + // double quotes, and these values come from scanned file content (a + // font-family name) or a file path, so untrusted input must be + // single-quoted (issue #476). Windows cmd.exe performs no such command + // substitution, but it treats a single quote as a literal character rather + // than a grouping delimiter, so a value or path containing spaces has to + // stay double-quoted there (Greptile #533). Keep the pre-existing + // double-quote escaping on Windows so that path's behavior is unchanged. + if (process.platform === 'win32') { + return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + return `'${text.replace(/'/g, `'\\''`)}'`; } function relativize(filePath, cwd) { diff --git a/plugin/skills/impeccable/scripts/lib/is-generated.mjs b/plugin/skills/impeccable/scripts/lib/is-generated.mjs index 165e1ca80..5e5948ad8 100644 --- a/plugin/skills/impeccable/scripts/lib/is-generated.mjs +++ b/plugin/skills/impeccable/scripts/lib/is-generated.mjs @@ -13,7 +13,7 @@ * within the first ~300 characters — catches non-git projects. */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) { function isGitIgnored(absPath, cwd) { try { - execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { + // argv form, never a shell: this runs on every file the live-mode source + // walk reaches, so a hostile filename embedding $(...) or backticks must + // not be interpretable (issue #476). JSON.stringify is not shell quoting. + execFileSync('git', ['check-ignore', '--quiet', absPath], { cwd, stdio: 'ignore', }); diff --git a/plugin/skills/impeccable/scripts/lib/staleness-deep.mjs b/plugin/skills/impeccable/scripts/lib/staleness-deep.mjs index 2c8d6a82f..f3ce76d9f 100644 --- a/plugin/skills/impeccable/scripts/lib/staleness-deep.mjs +++ b/plugin/skills/impeccable/scripts/lib/staleness-deep.mjs @@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/; // * bundle-relative: node ".agents/.../hook.mjs" // * legacy unquoted: node .claude/.../hook.mjs // * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical) -// * absolute: node "/Users/.../hook.mjs" (user-level installs) +// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since +// the shell-injection fix; older installs double-quote) // * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs" // A quoted path wins; the guard's two occurrences are identical, so the first // quoted match is the path. Otherwise fall back to the whitespace/metachar- @@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) { if (!HOOK_MARKER.test(str)) return null; const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/); if (quoted) return quoted[1]; + // A path containing an apostrophe serializes as '\'' inside single quotes; + // no regex reassembles that, and the bare fallback would misread a fragment + // of it, so return null: the caller never asserts on a path it can't parse. + if (str.includes("'\\''")) return null; + const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/); + if (singleQuoted) return singleQuoted[1]; const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/); return bare ? bare[1] : null; } diff --git a/plugin/skills/impeccable/scripts/live.mjs b/plugin/skills/impeccable/scripts/live.mjs index b04d98f50..7738c3f02 100644 --- a/plugin/skills/impeccable/scripts/live.mjs +++ b/plugin/skills/impeccable/scripts/live.mjs @@ -17,7 +17,7 @@ * node live.mjs --help */ -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -316,11 +316,17 @@ function globToRegex(pattern) { function runScript(name, args, options = {}) { const scriptPath = path.join(__dirname, name); - const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`; try { - return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 }); + // argv form, never a shell: string interpolation into double quotes would + // let a `"` or `$(...)` in any future caller's arg escape into the shell + // (issue #476). + return execFileSync(process.execPath, [scriptPath, ...args], { + encoding: 'utf-8', + cwd: options.cwd || process.cwd(), + timeout: 15_000, + }); } catch (err) { - // execSync throws on non-zero exit; return stdout if any + // execFileSync throws on non-zero exit; return stdout if any return err.stdout || err.message || ''; } } From 045865918afca96899a4aee2981b66bff6883a00 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Sat, 8 Aug 2026 19:26:43 -0700 Subject: [PATCH 29/35] Held for review: agent placeholder substitution, reviewer recapture contract, base-directory script form (#544) * Resolve {{scripts_path}} in the agent bodies Codex ships Three code paths emit an agent body: the degraded fallback reference, the .toml nested inside the skill for Codex, and the native agent file. Only the nested .toml skipped placeholder substitution and rule-marker stripping, so the codex and .agents dists shipped `node {{scripts_path}}/embed-prompt.mjs` verbatim in the asset producer, and every caller had to substitute the token itself at load time. All three now render through renderAgentBody(), and the new regression test asserts a runnable embed-prompt command on each emitted surface plus a synthetic agent proving markers and placeholders resolve in the nested .toml. Prepared by an AI agent (Claude Code) under pbakaus's instruction. Co-Authored-By: Claude Fable 5 * Give the finish reviewer's screenshots one fixed address The Input Contract asked for "desktop and mobile screenshot paths captured by the parent" and named none, so each session invented a filename and the verdict pass went looking for a recapture that was never written there. Two reviewer passes burned on that in the eval runs. The parent now captures and recaptures to .impeccable/review/desktop.png and .impeccable/review/mobile.png, and the reviewer reads those two first, treating a brief-named path as the fallback for a parent that wrote elsewhere. Prepared by an AI agent (Claude Code) under pbakaus's instruction. Co-Authored-By: Claude Fable 5 * Lead Setup with the base directory the runtime reports The rendered claude and codex skills opened with `node .claude/skills/impeccable/scripts/context.mjs`, a project-relative path that resolves in this repo and in nothing a user installs: a personal or plugin install puts the scripts outside the project entirely. The working form was already in the text, parenthesized, after the one that fails. Setup now leads with `node /scripts/context.mjs` and says once that the base directory resolves every scripts-path command in the skill and its references, leaving the project-relative path as the fallback for runtimes that report no base directory. Prepared by an AI agent (Claude Code) under pbakaus's instruction. Co-Authored-By: Claude Fable 5 * Answer the Copilot review: brittle model assertion, missing review dir Assert that {{model}} resolved rather than that it resolved to "GPT", which belongs to PROVIDER_PLACEHOLDERS and can change without touching what the test guards. And have the parent create .impeccable/review/ when the harness does not, so a fresh project's first capture has somewhere to land. Prepared by an AI agent (Claude Code) under pbakaus's instruction. Co-Authored-By: Claude Fable 5 * Make the review-screenshot contract directory-based, not web-viewport-named Two amendments to the recapture contract from review feedback: 1. The canonical location is the directory .impeccable/review/, one file per captured viewport; desktop.png and mobile.png are the web case, not the contract. Baking web-viewport names into the reviewer's spec would have hardened a web assumption into paths that a native (ios/android/adaptive) build cannot honestly write. 2. Precedence restored to explicit-beats-convention: paths the calling brief names are authoritative when the files exist; the canonical directory is where the reviewer looks when the brief names none or a named path is missing. This avoids stale canonical files from an earlier run silently winning over fresh explicit paths. The observed failure (the verdict round inventing a round-stamped filename) stays fixed: recapture happens over the same files, and invented filenames are still called out as pointing at nothing. Assisted-by: Claude Code --------- Co-authored-by: Claude Fable 5 --- scripts/lib/transformers/factory.js | 29 +++++--- skill/SKILL.src.md | 2 +- skill/agents/impeccable-finish-reviewer.md | 4 +- skill/reference/new-work.md | 2 +- tests/build.test.js | 84 ++++++++++++++++++++++ 5 files changed, 107 insertions(+), 14 deletions(-) diff --git a/scripts/lib/transformers/factory.js b/scripts/lib/transformers/factory.js index bf792d8bc..195e1b159 100644 --- a/scripts/lib/transformers/factory.js +++ b/scripts/lib/transformers/factory.js @@ -176,6 +176,22 @@ function buildCursorAgent(agent, body) { return `${generateYamlFrontmatter(frontmatter)}\n${body.trim()}\n`; } +/** + * Render an agent's markdown body for one provider. + * + * Every surface that ships an agent body (the degraded fallback reference, the + * Codex .toml nested inside the skill, and the native agent file) goes through + * here, so all three resolve provider blocks, {{placeholders}}, rule markers, + * and {{scripts_path}} the same way. The nested Codex .toml used to skip the + * last two and shipped `node {{scripts_path}}/embed-prompt.mjs` literally. + */ +function renderAgentBody(agent, { providerTags, placeholderKey, allSkillNames, scriptsPath }) { + let body = compileProviderBlocks(agent.body, providerTags); + body = replacePlaceholders(body, placeholderKey, [], allSkillNames); + body = stripRuleMarkers(body); + return body.replace(/\{\{scripts_path\}\}/g, scriptsPath); +} + function buildAgentFile(config, agent, body) { if (config.agentFormat === 'codex-toml') { return { @@ -330,10 +346,7 @@ export function createTransformer(config) { ensureDir(degradedDir); for (const agent of skill.agents) { const role = agent.name.replace(/^impeccable-/, ''); - let body = compileProviderBlocks(agent.body, providerTags); - body = replacePlaceholders(body, placeholderKey, [], allSkillNames); - body = stripRuleMarkers(body); - body = body.replace(/\{\{scripts_path\}\}/g, scriptsPath); + const body = renderAgentBody(agent, { providerTags, placeholderKey, allSkillNames, scriptsPath }); const content = `${DEGRADED_PREAMBLE}\n\n${body.replace(/^\s+/, '')}`; writeFile(path.join(degradedDir, `${role}.md`), content); refCount++; @@ -358,8 +371,7 @@ export function createTransformer(config) { if (CODEX_SKILL_PROVIDERS.has(provider)) { for (const agent of skill.agents || []) { if (agent.providers && !agent.providers.includes('codex')) continue; - let agentBody = compileProviderBlocks(agent.body, providerTags); - agentBody = replacePlaceholders(agentBody, placeholderKey, [], allSkillNames); + const agentBody = renderAgentBody(agent, { providerTags, placeholderKey, allSkillNames, scriptsPath }); const filename = `${agent.codexName || agent.name.replace(/-/g, '_')}.toml`; ensureDir(path.join(skillDir, 'agents')); writeFile(path.join(skillDir, 'agents', filename), buildCodexAgent(agent, agentBody)); @@ -375,10 +387,7 @@ export function createTransformer(config) { // Agents can declare `providers: ` to limit which harnesses // they emit to. Default (no field) ships everywhere with agentFormat. if (agent.providers && !agent.providers.includes(provider)) continue; - let body = compileProviderBlocks(agent.body, providerTags); - body = replacePlaceholders(body, placeholderKey, [], allSkillNames); - body = stripRuleMarkers(body); - body = body.replace(/\{\{scripts_path\}\}/g, scriptsPath); + const body = renderAgentBody(agent, { providerTags, placeholderKey, allSkillNames, scriptsPath }); const agentFile = buildAgentFile(config, agent, body); if (!agentFile) continue; ensureDir(agentsDir); diff --git a/skill/SKILL.src.md b/skill/SKILL.src.md index 59e56f09c..004a1b03e 100644 --- a/skill/SKILL.src.md +++ b/skill/SKILL.src.md @@ -18,7 +18,7 @@ Core principles: ## Setup -1. Run `node {{scripts_path}}/context.mjs` once per session (if the runtime shows this skill's loaded base directory, run `node /scripts/context.mjs`; keep cwd at the user's project). Pass a named source file or route as `--target `. It loads PRODUCT.md, DESIGN.md, the matching surface brief, and native-platform guidance when applicable; follow its directives and do not rerun it. +1. Run `node /scripts/context.mjs` once per session, where `` is the loaded base directory the runtime reports for this skill; keep cwd at the user's project. That base directory resolves every `node {{scripts_path}}/...` command in this skill and its references, and `{{scripts_path}}` is the fallback only when the runtime reports no base directory. Pass a named source file or route as `--target `. It loads PRODUCT.md, DESIGN.md, the matching surface brief, and native-platform guidance when applicable; follow its directives and do not rerun it. 2. Before acting, load the one playbook that owns the request: the Commands table's reference for an explicit or clearly implied sub-command, or [reference/new-work.md](reference/new-work.md) for a new surface or replacement visual world. Then inspect the target and at least one representative source of incumbent visual truth (tokens, theme, CSS, component, or asset) before editing. 3. After analysis and direction are resolved, load [reference/craft-floor.md](reference/craft-floor.md) immediately before editing UI. It carries the quality floor, the absolute bans, and the reflexes no detector catches. Do not load it for planning-only work. diff --git a/skill/agents/impeccable-finish-reviewer.md b/skill/agents/impeccable-finish-reviewer.md index 588c1545e..ed7e82fc7 100644 --- a/skill/agents/impeccable-finish-reviewer.md +++ b/skill/agents/impeccable-finish-reviewer.md @@ -22,7 +22,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order @@ -45,4 +45,4 @@ Return the disposition line first, then exactly five sections: `persistence` (pa ## Verdict Pass -When the parent returns with post-fix recaptures, you are scoring, not re-hunting. The parent's narration of what was fixed is not evidence; a claimed fix you cannot see in the recaptures is unresolved. For each material fix from your review, one line: resolved, partial, or unresolved, tied to what the new screenshots visibly show; a fix answered mechanically, positions moved but the quality the finding named still absent, is partial at best. Then name at most three regressions the fix batch itself introduced, judged by the same matrix rules, and nothing else; no new hunt, no new checks. Return exactly two sections: `verdict` (the scored list) and `remaining` (what stays open, or "clear"), and end with the disposition line recomputed against what remains open; unresolved or partial material findings can never recompute to ship. +When the parent returns with post-fix recaptures, you are scoring, not re-hunting. The parent recaptures over the same screenshot files you read in the review round, so re-read those exact paths for this round; a round-stamped filename you invent points at nothing. The parent's narration of what was fixed is not evidence; a claimed fix you cannot see in the recaptures is unresolved. For each material fix from your review, one line: resolved, partial, or unresolved, tied to what the new screenshots visibly show; a fix answered mechanically, positions moved but the quality the finding named still absent, is partial at best. Then name at most three regressions the fix batch itself introduced, judged by the same matrix rules, and nothing else; no new hunt, no new checks. Return exactly two sections: `verdict` (the scored list) and `remaining` (what stays open, or "clear"), and end with the disposition line recomputed against what remains open; unresolved or partial material findings can never recompute to ship. diff --git a/skill/reference/new-work.md b/skill/reference/new-work.md index adddcc12e..d745fdf5d 100644 --- a/skill/reference/new-work.md +++ b/skill/reference/new-work.md @@ -109,6 +109,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node {{scripts_path}}/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node {{scripts_path}}/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/tests/build.test.js b/tests/build.test.js index 44a21dd8d..6f6865237 100644 --- a/tests/build.test.js +++ b/tests/build.test.js @@ -647,3 +647,87 @@ describe('Cursor subagent generation', () => { expect(assetProducer).toContain('.cursor/skills/impeccable/scripts'); }); }); + +// Regression guard for the gap that shipped literal `{{scripts_path}}` inside +// the Codex dists' nested agent .toml: three separate code paths emit an agent +// body, and one of them skipped placeholder substitution and rule-marker +// stripping. Assert every surface, not just the one that was broken. +describe('agent bodies resolve placeholders on every surface that ships them', () => { + const ROOT = process.cwd(); + const AGENT_TEST_DIR = path.join(ROOT, 'test-tmp-agent-placeholders'); + const DIST = path.join(AGENT_TEST_DIR, 'dist'); + + // [emitted file, the scripts path that provider installs to] + const SURFACES = [ + // Nested Codex .toml: the skill install is the whole delivery for these. + ['codex/.codex/skills/impeccable/agents/impeccable_asset_producer.toml', '.codex/skills/impeccable/scripts'], + ['agents/.agents/skills/impeccable/agents/impeccable_asset_producer.toml', '.agents/skills/impeccable/scripts'], + // Native agent files. + ['claude-code/.claude/agents/impeccable-asset-producer.md', '.claude/skills/impeccable/scripts'], + ['github/.github/agents/impeccable-asset-producer.agent.md', '.github/skills/impeccable/scripts'], + ['grok/.grok/agents/impeccable-asset-producer.md', '.grok/skills/impeccable/scripts'], + // Degraded fallback reference generated from the same agent definition. + ['codex/.codex/skills/impeccable/reference/degraded/asset-producer.md', '.codex/skills/impeccable/scripts'], + ]; + + beforeEach(() => { + if (fs.existsSync(AGENT_TEST_DIR)) fs.rmSync(AGENT_TEST_DIR, { recursive: true, force: true }); + fs.mkdirSync(AGENT_TEST_DIR, { recursive: true }); + const { skills } = utils.readSourceFiles(ROOT); + transformers.transformCodex(skills, DIST); + transformers.transformAgents(skills, DIST); + transformers.transformClaudeCode(skills, DIST); + transformers.transformGitHub(skills, DIST); + transformers.transformGrok(skills, DIST); + }); + + afterEach(() => { + if (fs.existsSync(AGENT_TEST_DIR)) fs.rmSync(AGENT_TEST_DIR, { recursive: true, force: true }); + }); + + test('the asset producer ships a runnable embed-prompt command, never the raw token', () => { + for (const [relPath, scriptsPath] of SURFACES) { + const content = fs.readFileSync(path.join(DIST, relPath), 'utf-8'); + expect(content).toContain(`node ${scriptsPath}/embed-prompt.mjs`); + expect(content).not.toContain('{{scripts_path}}'); + } + }); + + test('no emitted agent body carries an unresolved placeholder or a rule marker', () => { + const synthetic = { + name: 'impeccable', + description: 'synthetic', + body: 'Synthetic skill body.', + agents: [ + { + name: 'impeccable-synthetic', + codexName: 'impeccable_synthetic', + description: 'synthetic agent', + body: 'Run `node {{scripts_path}}/embed-prompt.mjs` and ask {{model}}. ', + }, + ], + }; + const synthDist = path.join(AGENT_TEST_DIR, 'synth'); + transformers.transformCodex([synthetic], synthDist); + transformers.transformClaudeCode([synthetic], synthDist); + + const emitted = [ + 'codex/.codex/skills/impeccable/agents/impeccable_synthetic.toml', + 'codex/.codex/skills/impeccable/reference/degraded/synthetic.md', + 'claude-code/.claude/agents/impeccable-synthetic.md', + ]; + for (const relPath of emitted) { + const content = fs.readFileSync(path.join(synthDist, relPath), 'utf-8'); + expect(content).not.toContain('{{'); + expect(content).not.toMatch(/ - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/skill/reference/ios.md b/skill/reference/ios.md index 9d2ba20a0..bff06b2a3 100644 --- a/skill/reference/ios.md +++ b/skill/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/skill/reference/new-work.md b/skill/reference/new-work.md index d745fdf5d..f9b785dcd 100644 --- a/skill/reference/new-work.md +++ b/skill/reference/new-work.md @@ -107,8 +107,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node {{scripts_path}}/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node {{scripts_path}}/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. From c38ad8fb8b63aea7eccd316a919f62ea9142f3ab Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 9 Aug 2026 22:10:23 +0000 Subject: [PATCH 32/35] Sync generated provider output --- .agents/skills/impeccable/SKILL.md | 2 +- .../impeccable/agents/impeccable_finish_reviewer.toml | 2 +- .agents/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .agents/skills/impeccable/reference/ios.md | 6 ++++++ .agents/skills/impeccable/reference/new-work.md | 4 ++-- .claude/agents/impeccable-finish-reviewer.md | 2 +- .claude/skills/impeccable/SKILL.md | 2 +- .claude/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .claude/skills/impeccable/reference/ios.md | 6 ++++++ .claude/skills/impeccable/reference/new-work.md | 4 ++-- .cursor/agents/impeccable-finish-reviewer.md | 2 +- .cursor/skills/impeccable/SKILL.md | 2 +- .cursor/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .cursor/skills/impeccable/reference/ios.md | 6 ++++++ .cursor/skills/impeccable/reference/new-work.md | 4 ++-- .gemini/skills/impeccable/SKILL.md | 2 +- .gemini/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .gemini/skills/impeccable/reference/ios.md | 6 ++++++ .gemini/skills/impeccable/reference/new-work.md | 4 ++-- .github/skills/impeccable/SKILL.md | 2 +- .github/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .github/skills/impeccable/reference/ios.md | 6 ++++++ .github/skills/impeccable/reference/new-work.md | 4 ++-- .grok/agents/impeccable-finish-reviewer.md | 2 +- .grok/skills/impeccable/SKILL.md | 2 +- .grok/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .grok/skills/impeccable/reference/ios.md | 6 ++++++ .grok/skills/impeccable/reference/new-work.md | 4 ++-- .kiro/skills/impeccable/SKILL.md | 2 +- .kiro/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .kiro/skills/impeccable/reference/ios.md | 6 ++++++ .kiro/skills/impeccable/reference/new-work.md | 4 ++-- .opencode/skills/impeccable/SKILL.md | 2 +- .opencode/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .opencode/skills/impeccable/reference/ios.md | 6 ++++++ .opencode/skills/impeccable/reference/new-work.md | 4 ++-- .pi/skills/impeccable/SKILL.md | 2 +- .pi/skills/impeccable/reference/android.md | 6 ++++++ .pi/skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .pi/skills/impeccable/reference/ios.md | 6 ++++++ .pi/skills/impeccable/reference/new-work.md | 4 ++-- .qoder/skills/impeccable/SKILL.md | 2 +- .qoder/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .qoder/skills/impeccable/reference/ios.md | 6 ++++++ .qoder/skills/impeccable/reference/new-work.md | 4 ++-- .rovodev/skills/impeccable/SKILL.md | 2 +- .rovodev/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .rovodev/skills/impeccable/reference/ios.md | 6 ++++++ .rovodev/skills/impeccable/reference/new-work.md | 4 ++-- .trae-cn/skills/impeccable/SKILL.md | 2 +- .trae-cn/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .trae-cn/skills/impeccable/reference/ios.md | 6 ++++++ .trae-cn/skills/impeccable/reference/new-work.md | 4 ++-- .trae/skills/impeccable/SKILL.md | 2 +- .trae/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .trae/skills/impeccable/reference/ios.md | 6 ++++++ .trae/skills/impeccable/reference/new-work.md | 4 ++-- .vibe/skills/impeccable/SKILL.md | 2 +- .vibe/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- .vibe/skills/impeccable/reference/ios.md | 6 ++++++ .vibe/skills/impeccable/reference/new-work.md | 4 ++-- plugin/agents/impeccable-finish-reviewer.md | 2 +- plugin/skills/impeccable/SKILL.md | 2 +- plugin/skills/impeccable/reference/android.md | 6 ++++++ .../skills/impeccable/reference/degraded/finish-reviewer.md | 2 +- plugin/skills/impeccable/reference/ios.md | 6 ++++++ plugin/skills/impeccable/reference/new-work.md | 4 ++-- 80 files changed, 245 insertions(+), 65 deletions(-) diff --git a/.agents/skills/impeccable/SKILL.md b/.agents/skills/impeccable/SKILL.md index 347501caf..72f07c311 100644 --- a/.agents/skills/impeccable/SKILL.md +++ b/.agents/skills/impeccable/SKILL.md @@ -9,7 +9,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.agents/skills/impeccable/agents/impeccable_finish_reviewer.toml b/.agents/skills/impeccable/agents/impeccable_finish_reviewer.toml index 833b511aa..91b45993d 100644 --- a/.agents/skills/impeccable/agents/impeccable_finish_reviewer.toml +++ b/.agents/skills/impeccable/agents/impeccable_finish_reviewer.toml @@ -13,7 +13,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.agents/skills/impeccable/reference/android.md b/.agents/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.agents/skills/impeccable/reference/android.md +++ b/.agents/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.agents/skills/impeccable/reference/degraded/finish-reviewer.md b/.agents/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.agents/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.agents/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.agents/skills/impeccable/reference/ios.md b/.agents/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.agents/skills/impeccable/reference/ios.md +++ b/.agents/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.agents/skills/impeccable/reference/new-work.md b/.agents/skills/impeccable/reference/new-work.md index ab8059c90..ca489abf8 100644 --- a/.agents/skills/impeccable/reference/new-work.md +++ b/.agents/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .agents/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .agents/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.claude/agents/impeccable-finish-reviewer.md b/.claude/agents/impeccable-finish-reviewer.md index 76f830597..d03529403 100644 --- a/.claude/agents/impeccable-finish-reviewer.md +++ b/.claude/agents/impeccable-finish-reviewer.md @@ -16,7 +16,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.claude/skills/impeccable/SKILL.md b/.claude/skills/impeccable/SKILL.md index ee8937bc4..43920ae2b 100644 --- a/.claude/skills/impeccable/SKILL.md +++ b/.claude/skills/impeccable/SKILL.md @@ -15,7 +15,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.claude/skills/impeccable/reference/android.md b/.claude/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.claude/skills/impeccable/reference/android.md +++ b/.claude/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.claude/skills/impeccable/reference/degraded/finish-reviewer.md b/.claude/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.claude/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.claude/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.claude/skills/impeccable/reference/ios.md b/.claude/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.claude/skills/impeccable/reference/ios.md +++ b/.claude/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.claude/skills/impeccable/reference/new-work.md b/.claude/skills/impeccable/reference/new-work.md index 9a70a0b8e..859b84a6b 100644 --- a/.claude/skills/impeccable/reference/new-work.md +++ b/.claude/skills/impeccable/reference/new-work.md @@ -105,8 +105,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .claude/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .claude/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.cursor/agents/impeccable-finish-reviewer.md b/.cursor/agents/impeccable-finish-reviewer.md index 1cdf3f3a9..544a1e464 100644 --- a/.cursor/agents/impeccable-finish-reviewer.md +++ b/.cursor/agents/impeccable-finish-reviewer.md @@ -15,7 +15,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.cursor/skills/impeccable/SKILL.md b/.cursor/skills/impeccable/SKILL.md index c4af4ee53..7584a45ff 100644 --- a/.cursor/skills/impeccable/SKILL.md +++ b/.cursor/skills/impeccable/SKILL.md @@ -10,7 +10,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.cursor/skills/impeccable/reference/android.md b/.cursor/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.cursor/skills/impeccable/reference/android.md +++ b/.cursor/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.cursor/skills/impeccable/reference/degraded/finish-reviewer.md b/.cursor/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.cursor/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.cursor/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.cursor/skills/impeccable/reference/ios.md b/.cursor/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.cursor/skills/impeccable/reference/ios.md +++ b/.cursor/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.cursor/skills/impeccable/reference/new-work.md b/.cursor/skills/impeccable/reference/new-work.md index de8878bd9..e519eb9cd 100644 --- a/.cursor/skills/impeccable/reference/new-work.md +++ b/.cursor/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .cursor/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .cursor/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.gemini/skills/impeccable/SKILL.md b/.gemini/skills/impeccable/SKILL.md index ba895a5c9..62bdd7fc6 100644 --- a/.gemini/skills/impeccable/SKILL.md +++ b/.gemini/skills/impeccable/SKILL.md @@ -9,7 +9,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.gemini/skills/impeccable/reference/android.md b/.gemini/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.gemini/skills/impeccable/reference/android.md +++ b/.gemini/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.gemini/skills/impeccable/reference/degraded/finish-reviewer.md b/.gemini/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.gemini/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.gemini/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.gemini/skills/impeccable/reference/ios.md b/.gemini/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.gemini/skills/impeccable/reference/ios.md +++ b/.gemini/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.gemini/skills/impeccable/reference/new-work.md b/.gemini/skills/impeccable/reference/new-work.md index 8874c2486..93840d013 100644 --- a/.gemini/skills/impeccable/reference/new-work.md +++ b/.gemini/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .gemini/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .gemini/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.github/skills/impeccable/SKILL.md b/.github/skills/impeccable/SKILL.md index 6c45dfc99..b53ababd1 100644 --- a/.github/skills/impeccable/SKILL.md +++ b/.github/skills/impeccable/SKILL.md @@ -12,7 +12,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.github/skills/impeccable/reference/android.md b/.github/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.github/skills/impeccable/reference/android.md +++ b/.github/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.github/skills/impeccable/reference/degraded/finish-reviewer.md b/.github/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.github/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.github/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.github/skills/impeccable/reference/ios.md b/.github/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.github/skills/impeccable/reference/ios.md +++ b/.github/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.github/skills/impeccable/reference/new-work.md b/.github/skills/impeccable/reference/new-work.md index 75d8133ad..e39008588 100644 --- a/.github/skills/impeccable/reference/new-work.md +++ b/.github/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .github/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .github/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.grok/agents/impeccable-finish-reviewer.md b/.grok/agents/impeccable-finish-reviewer.md index 76f830597..d03529403 100644 --- a/.grok/agents/impeccable-finish-reviewer.md +++ b/.grok/agents/impeccable-finish-reviewer.md @@ -16,7 +16,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.grok/skills/impeccable/SKILL.md b/.grok/skills/impeccable/SKILL.md index 09a1a4797..d0f9c8142 100644 --- a/.grok/skills/impeccable/SKILL.md +++ b/.grok/skills/impeccable/SKILL.md @@ -15,7 +15,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.grok/skills/impeccable/reference/android.md b/.grok/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.grok/skills/impeccable/reference/android.md +++ b/.grok/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.grok/skills/impeccable/reference/degraded/finish-reviewer.md b/.grok/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.grok/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.grok/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.grok/skills/impeccable/reference/ios.md b/.grok/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.grok/skills/impeccable/reference/ios.md +++ b/.grok/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.grok/skills/impeccable/reference/new-work.md b/.grok/skills/impeccable/reference/new-work.md index f3aacee42..884db3fd0 100644 --- a/.grok/skills/impeccable/reference/new-work.md +++ b/.grok/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .grok/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .grok/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.kiro/skills/impeccable/SKILL.md b/.kiro/skills/impeccable/SKILL.md index bdbd7bf63..0b23316a7 100644 --- a/.kiro/skills/impeccable/SKILL.md +++ b/.kiro/skills/impeccable/SKILL.md @@ -10,7 +10,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.kiro/skills/impeccable/reference/android.md b/.kiro/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.kiro/skills/impeccable/reference/android.md +++ b/.kiro/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.kiro/skills/impeccable/reference/degraded/finish-reviewer.md b/.kiro/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.kiro/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.kiro/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.kiro/skills/impeccable/reference/ios.md b/.kiro/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.kiro/skills/impeccable/reference/ios.md +++ b/.kiro/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.kiro/skills/impeccable/reference/new-work.md b/.kiro/skills/impeccable/reference/new-work.md index d5d6b99b6..0b7bd950b 100644 --- a/.kiro/skills/impeccable/reference/new-work.md +++ b/.kiro/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .kiro/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .kiro/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.opencode/skills/impeccable/SKILL.md b/.opencode/skills/impeccable/SKILL.md index f111787da..5c20ef199 100644 --- a/.opencode/skills/impeccable/SKILL.md +++ b/.opencode/skills/impeccable/SKILL.md @@ -15,7 +15,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.opencode/skills/impeccable/reference/android.md b/.opencode/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.opencode/skills/impeccable/reference/android.md +++ b/.opencode/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.opencode/skills/impeccable/reference/degraded/finish-reviewer.md b/.opencode/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.opencode/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.opencode/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.opencode/skills/impeccable/reference/ios.md b/.opencode/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.opencode/skills/impeccable/reference/ios.md +++ b/.opencode/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.opencode/skills/impeccable/reference/new-work.md b/.opencode/skills/impeccable/reference/new-work.md index 5848ad74b..09792494c 100644 --- a/.opencode/skills/impeccable/reference/new-work.md +++ b/.opencode/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .opencode/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .opencode/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.pi/skills/impeccable/SKILL.md b/.pi/skills/impeccable/SKILL.md index 2916526aa..460996938 100644 --- a/.pi/skills/impeccable/SKILL.md +++ b/.pi/skills/impeccable/SKILL.md @@ -13,7 +13,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.pi/skills/impeccable/reference/android.md b/.pi/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.pi/skills/impeccable/reference/android.md +++ b/.pi/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.pi/skills/impeccable/reference/degraded/finish-reviewer.md b/.pi/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.pi/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.pi/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.pi/skills/impeccable/reference/ios.md b/.pi/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.pi/skills/impeccable/reference/ios.md +++ b/.pi/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.pi/skills/impeccable/reference/new-work.md b/.pi/skills/impeccable/reference/new-work.md index 78e86c1bb..396dee5fc 100644 --- a/.pi/skills/impeccable/reference/new-work.md +++ b/.pi/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .pi/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .pi/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.qoder/skills/impeccable/SKILL.md b/.qoder/skills/impeccable/SKILL.md index a418eb317..cbef14a60 100644 --- a/.qoder/skills/impeccable/SKILL.md +++ b/.qoder/skills/impeccable/SKILL.md @@ -15,7 +15,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.qoder/skills/impeccable/reference/android.md b/.qoder/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.qoder/skills/impeccable/reference/android.md +++ b/.qoder/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.qoder/skills/impeccable/reference/degraded/finish-reviewer.md b/.qoder/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.qoder/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.qoder/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.qoder/skills/impeccable/reference/ios.md b/.qoder/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.qoder/skills/impeccable/reference/ios.md +++ b/.qoder/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.qoder/skills/impeccable/reference/new-work.md b/.qoder/skills/impeccable/reference/new-work.md index 71ab4796e..286b9ad45 100644 --- a/.qoder/skills/impeccable/reference/new-work.md +++ b/.qoder/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .qoder/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .qoder/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.rovodev/skills/impeccable/SKILL.md b/.rovodev/skills/impeccable/SKILL.md index c1d0806fa..648660e93 100644 --- a/.rovodev/skills/impeccable/SKILL.md +++ b/.rovodev/skills/impeccable/SKILL.md @@ -15,7 +15,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.rovodev/skills/impeccable/reference/android.md b/.rovodev/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.rovodev/skills/impeccable/reference/android.md +++ b/.rovodev/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.rovodev/skills/impeccable/reference/degraded/finish-reviewer.md b/.rovodev/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.rovodev/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.rovodev/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.rovodev/skills/impeccable/reference/ios.md b/.rovodev/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.rovodev/skills/impeccable/reference/ios.md +++ b/.rovodev/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.rovodev/skills/impeccable/reference/new-work.md b/.rovodev/skills/impeccable/reference/new-work.md index 29218d01f..47f451c6b 100644 --- a/.rovodev/skills/impeccable/reference/new-work.md +++ b/.rovodev/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .rovodev/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .rovodev/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.trae-cn/skills/impeccable/SKILL.md b/.trae-cn/skills/impeccable/SKILL.md index b622286b3..84992102d 100644 --- a/.trae-cn/skills/impeccable/SKILL.md +++ b/.trae-cn/skills/impeccable/SKILL.md @@ -12,7 +12,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.trae-cn/skills/impeccable/reference/android.md b/.trae-cn/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.trae-cn/skills/impeccable/reference/android.md +++ b/.trae-cn/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.trae-cn/skills/impeccable/reference/degraded/finish-reviewer.md b/.trae-cn/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.trae-cn/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.trae-cn/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.trae-cn/skills/impeccable/reference/ios.md b/.trae-cn/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.trae-cn/skills/impeccable/reference/ios.md +++ b/.trae-cn/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.trae-cn/skills/impeccable/reference/new-work.md b/.trae-cn/skills/impeccable/reference/new-work.md index 96faf817a..1bd18cc6c 100644 --- a/.trae-cn/skills/impeccable/reference/new-work.md +++ b/.trae-cn/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .trae-cn/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .trae-cn/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.trae/skills/impeccable/SKILL.md b/.trae/skills/impeccable/SKILL.md index 7c7640b97..805d762c8 100644 --- a/.trae/skills/impeccable/SKILL.md +++ b/.trae/skills/impeccable/SKILL.md @@ -12,7 +12,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.trae/skills/impeccable/reference/android.md b/.trae/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.trae/skills/impeccable/reference/android.md +++ b/.trae/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.trae/skills/impeccable/reference/degraded/finish-reviewer.md b/.trae/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.trae/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.trae/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.trae/skills/impeccable/reference/ios.md b/.trae/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.trae/skills/impeccable/reference/ios.md +++ b/.trae/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.trae/skills/impeccable/reference/new-work.md b/.trae/skills/impeccable/reference/new-work.md index c60a1247b..aa6defef0 100644 --- a/.trae/skills/impeccable/reference/new-work.md +++ b/.trae/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .trae/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .trae/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.vibe/skills/impeccable/SKILL.md b/.vibe/skills/impeccable/SKILL.md index d3a201210..780ccdec0 100644 --- a/.vibe/skills/impeccable/SKILL.md +++ b/.vibe/skills/impeccable/SKILL.md @@ -14,7 +14,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/.vibe/skills/impeccable/reference/android.md b/.vibe/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/.vibe/skills/impeccable/reference/android.md +++ b/.vibe/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/.vibe/skills/impeccable/reference/degraded/finish-reviewer.md b/.vibe/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/.vibe/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/.vibe/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/.vibe/skills/impeccable/reference/ios.md b/.vibe/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/.vibe/skills/impeccable/reference/ios.md +++ b/.vibe/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/.vibe/skills/impeccable/reference/new-work.md b/.vibe/skills/impeccable/reference/new-work.md index b426805a4..6d625bd64 100644 --- a/.vibe/skills/impeccable/reference/new-work.md +++ b/.vibe/skills/impeccable/reference/new-work.md @@ -103,8 +103,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .vibe/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .vibe/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/plugin/agents/impeccable-finish-reviewer.md b/plugin/agents/impeccable-finish-reviewer.md index 76f830597..d03529403 100644 --- a/plugin/agents/impeccable-finish-reviewer.md +++ b/plugin/agents/impeccable-finish-reviewer.md @@ -16,7 +16,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/plugin/skills/impeccable/SKILL.md b/plugin/skills/impeccable/SKILL.md index ee8937bc4..43920ae2b 100644 --- a/plugin/skills/impeccable/SKILL.md +++ b/plugin/skills/impeccable/SKILL.md @@ -15,7 +15,7 @@ This skill gives you the tools and permission to create design that earns to be Core principles: - Go all out. No hedging, no shortcuts. The deliverable must be complete (except assets the user must provide). - Dream big and bold. Distinct, beautiful, outstanding and highly inspiring work. -- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. +- Verify in bounded passes, not a loop, and the ceiling covers the whole cycle: screenshots, defect scans, micro-edits, and rebuilds alike. Build fully, inspect once with a batched round (desktop and mobile together on the web; the shipped device classes on a native platform), fix everything it shows in one batch, confirm with at most one more round, and stop polishing. Open-ended self-QA burns the user's money doing worse what the finish handoffs do better. ## Setup diff --git a/plugin/skills/impeccable/reference/android.md b/plugin/skills/impeccable/reference/android.md index 6337b9018..1f67a6bb5 100644 --- a/plugin/skills/impeccable/reference/android.md +++ b/plugin/skills/impeccable/reference/android.md @@ -38,3 +38,9 @@ Would a fluent Android user trust this app, or trip on off-spec components? The - **One FAB, one primary action.** Never stack FABs or spend one on a secondary task. - **Snackbars for transient feedback** (actionable when useful, never a toast for that); dialogs only for decisions that must interrupt. - **Material motion patterns.** Container transform, shared-axis, fade-through, with standard easing and durations; honor the system Remove animations setting with a crossfade or instant cut. + +## Verifying the build + +- **Screenshots come from the emulator or a connected device, never a browser.** Build and install, then capture with `adb exec-out screencap -p > ` (pick a device with `adb -s ` when several are attached). Capture every device class the app ships to, at least one phone and, when tablets are a target, one tablet, and write the files where the review flow expects them. +- **Dark theme and font scale belong in the pass.** `adb shell cmd uimode night yes` flips the theme; `adb shell settings put system font_scale 1.3` (restore `1.0` after) catches the clipped labels a fixed layout hides; with several targets attached, the capture's `-s ` goes on these commands too. +- **Emulators give breadth; gestures, refresh rates, and performance need hardware.** Say which one produced the evidence. diff --git a/plugin/skills/impeccable/reference/degraded/finish-reviewer.md b/plugin/skills/impeccable/reference/degraded/finish-reviewer.md index a97099a39..e90fd9f20 100644 --- a/plugin/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/plugin/skills/impeccable/reference/degraded/finish-reviewer.md @@ -11,7 +11,7 @@ A hard turn ceiling ends the run without warning; a run that ends before the fiv ## Input Contract -Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. +Expect: the original request; the confirmed user answers; the artifact path(s); the screenshots the parent captured, which live in `.impeccable/review/` (on the web, `desktop.png` and `mobile.png`; on native, device-class names such as `phone.png` and `tablet.png`, suffixed per OS on adaptive); a screenshot path the calling brief names is authoritative when the file exists, and `.impeccable/review/` is where to look when the brief names none or a named path is missing, never a filename you invent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and, on a comp-led build, the approved comp path (a code-led build has no approved comp; it passes the chosen decision comp as a separate critique-reference input, labeled as such, and nothing in this file that binds “the approved comp” binds it); and the skill's `reference/craft-floor.md` path. On a native (`ios` / `android` / `adaptive`) build the packet also carries the platform reference path(s) (`reference/ios.md` / `reference/android.md`) and a line saying no detector ran: read the platform reference alongside the craft floor and judge every check in the platform's own conventions, the screenshots are device captures rather than browser viewports, and your floor check is the build's only slop gate. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: a review anchored on the contract inherits whatever the builder's abstraction dropped. ## Checks, in order diff --git a/plugin/skills/impeccable/reference/ios.md b/plugin/skills/impeccable/reference/ios.md index ccef5d2c4..c6244dfe3 100644 --- a/plugin/skills/impeccable/reference/ios.md +++ b/plugin/skills/impeccable/reference/ios.md @@ -43,3 +43,9 @@ Would a fluent iPhone user trust this app, or pause at off-spec controls? The te - **System transitions.** Push slides, sheets rise, dismiss reverses the entrance. Custom transitions that fight the navigation model disorient. - **Honor Reduce Motion.** Crossfade instead of parallax and large slides. + +## Verifying the build + +- **Screenshots come from the Simulator, never a browser.** Build and run, then capture with `xcrun simctl io booted screenshot ` (with several running, replace `booted` with the target's UDID from `xcrun simctl list devices booted`; display names can collide, the UDID never does). Capture every device class the app ships to, at least one iPhone and, when iPad is a target, one iPad, and write the files where the review flow expects them. +- **Dark Mode and Dynamic Type belong in the pass.** `xcrun simctl ui booted appearance dark` flips appearance, reusing the capture's UDID when several are booted; a check at a large Dynamic Type size catches the truncation a fixed layout hides. +- **Simulators give breadth; posture, gestures, and performance need hardware.** Say which one produced the evidence. diff --git a/plugin/skills/impeccable/reference/new-work.md b/plugin/skills/impeccable/reference/new-work.md index 9a70a0b8e..859b84a6b 100644 --- a/plugin/skills/impeccable/reference/new-work.md +++ b/plugin/skills/impeccable/reference/new-work.md @@ -105,8 +105,8 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi ## 7. Inspect and finish -Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. +Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. When an approved comp exists, the critique is a side-by-side: view the comp region and the build region together, the hero and each section as its own crop at legible scale, never one full-page thumbnail, which hides exactly the failures that matter, crude controls, wrong lettering character, flattened material, behind a superficially similar section order. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. -After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Where this harness runs no design hook, run `node .claude/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless build that skips this ships every tell the hook exists to catch. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), and the craft-floor reference path. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. +After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .claude/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, and that directory is where it looks when a passed path is missing. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (on a code-led build there is no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. When the reviewer's first material fix is a rebuild directive, fidelity failed wholesale rather than in patches, so skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a verdict, telling the user what is happening rather than asking permission to fix a failure. The user is consulted only when a second rebuild directive arrives, both verdicts on the table, or when rebuilding would discard content the user approved. Otherwise apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever is deciding, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included, under the reviewer's own disposition word: a table with open material findings is never announced as a pass, and never under a softer label than the reviewer wrote. Do not run a second detector. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. From 181212cb2dce0f9038acd99a1a9d19f040a19fba Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Sun, 9 Aug 2026 15:35:07 -0700 Subject: [PATCH 33/35] Map polish's evidence and verify steps per platform (#550) * Map polish's evidence and verify steps per platform polish.md was the last command reference verifying through web-only vocabulary after #546 gave the pipeline its native leg. Three targeted mappings, following the in-file precedent new-work.md set (the classify-triage-polish-verify flow itself is platform-neutral, so no polish.native.md): - Evidence gathering: desktop and mobile sizes on the web; the shipped device classes on simulator, emulator, or hardware on native, per the platform reference's Verifying the build section. - Verify checklist layouts: phone and tablet size classes, both orientations where supported, on native. - Verify checklist "supported browsers": native has none, so the analogues are named (runtime warnings, dropped frames, supported OS versions). Assisted-by: Claude Code * fix: branch the verify checklist web-vs-native explicitly Copilot follow-up: the parenthetical style could read as both term sets applying on native. The two bullets now branch explicitly, and the shared items (console errors, layout shift, latency, image loading) stay unbranched since they apply everywhere. Assisted-by: Claude Code * fix: restore runtime warnings to the native verify branch greptile follow-up: the explicit-branch restyle dropped the runtime warnings requirement the parenthetical carried; folding it into "console errors everywhere" hid it behind web vocabulary. It is back as its own item in the native branch. Assisted-by: Claude Code --- skill/reference/polish.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skill/reference/polish.md b/skill/reference/polish.md index d58ca3a49..1d2bfcff3 100644 --- a/skill/reference/polish.md +++ b/skill/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. From 29e5b1494c0e8f8eaa583ad10657f8564031313c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 9 Aug 2026 22:35:43 +0000 Subject: [PATCH 34/35] Sync generated provider output --- .agents/skills/impeccable/reference/polish.md | 6 +++--- .claude/skills/impeccable/reference/polish.md | 6 +++--- .cursor/skills/impeccable/reference/polish.md | 6 +++--- .gemini/skills/impeccable/reference/polish.md | 6 +++--- .github/skills/impeccable/reference/polish.md | 6 +++--- .grok/skills/impeccable/reference/polish.md | 6 +++--- .kiro/skills/impeccable/reference/polish.md | 6 +++--- .opencode/skills/impeccable/reference/polish.md | 6 +++--- .pi/skills/impeccable/reference/polish.md | 6 +++--- .qoder/skills/impeccable/reference/polish.md | 6 +++--- .rovodev/skills/impeccable/reference/polish.md | 6 +++--- .trae-cn/skills/impeccable/reference/polish.md | 6 +++--- .trae/skills/impeccable/reference/polish.md | 6 +++--- .vibe/skills/impeccable/reference/polish.md | 6 +++--- plugin/skills/impeccable/reference/polish.md | 6 +++--- 15 files changed, 45 insertions(+), 45 deletions(-) diff --git a/.agents/skills/impeccable/reference/polish.md b/.agents/skills/impeccable/reference/polish.md index b18014fe5..a9fd13935 100644 --- a/.agents/skills/impeccable/reference/polish.md +++ b/.agents/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.claude/skills/impeccable/reference/polish.md b/.claude/skills/impeccable/reference/polish.md index a23c3887f..9db878697 100644 --- a/.claude/skills/impeccable/reference/polish.md +++ b/.claude/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.cursor/skills/impeccable/reference/polish.md b/.cursor/skills/impeccable/reference/polish.md index f7ad2f44b..7af88f60c 100644 --- a/.cursor/skills/impeccable/reference/polish.md +++ b/.cursor/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.gemini/skills/impeccable/reference/polish.md b/.gemini/skills/impeccable/reference/polish.md index f3cfc9c39..877a83c84 100644 --- a/.gemini/skills/impeccable/reference/polish.md +++ b/.gemini/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.github/skills/impeccable/reference/polish.md b/.github/skills/impeccable/reference/polish.md index 7bdf7eb5c..0e115f981 100644 --- a/.github/skills/impeccable/reference/polish.md +++ b/.github/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.grok/skills/impeccable/reference/polish.md b/.grok/skills/impeccable/reference/polish.md index c9c14dd4d..e5ff9c4b5 100644 --- a/.grok/skills/impeccable/reference/polish.md +++ b/.grok/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.kiro/skills/impeccable/reference/polish.md b/.kiro/skills/impeccable/reference/polish.md index 391142469..fee049ce1 100644 --- a/.kiro/skills/impeccable/reference/polish.md +++ b/.kiro/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.opencode/skills/impeccable/reference/polish.md b/.opencode/skills/impeccable/reference/polish.md index 6e4709750..d46ea12c9 100644 --- a/.opencode/skills/impeccable/reference/polish.md +++ b/.opencode/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.pi/skills/impeccable/reference/polish.md b/.pi/skills/impeccable/reference/polish.md index fdb9ef52d..fb26c7fca 100644 --- a/.pi/skills/impeccable/reference/polish.md +++ b/.pi/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.qoder/skills/impeccable/reference/polish.md b/.qoder/skills/impeccable/reference/polish.md index ae688924a..b392fe8fd 100644 --- a/.qoder/skills/impeccable/reference/polish.md +++ b/.qoder/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.rovodev/skills/impeccable/reference/polish.md b/.rovodev/skills/impeccable/reference/polish.md index 21f624ece..b18f133ad 100644 --- a/.rovodev/skills/impeccable/reference/polish.md +++ b/.rovodev/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.trae-cn/skills/impeccable/reference/polish.md b/.trae-cn/skills/impeccable/reference/polish.md index 5bbe2215d..56789b001 100644 --- a/.trae-cn/skills/impeccable/reference/polish.md +++ b/.trae-cn/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.trae/skills/impeccable/reference/polish.md b/.trae/skills/impeccable/reference/polish.md index 2f5cdff3f..ffd4ea340 100644 --- a/.trae/skills/impeccable/reference/polish.md +++ b/.trae/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/.vibe/skills/impeccable/reference/polish.md b/.vibe/skills/impeccable/reference/polish.md index bad65d956..7e7d52c3f 100644 --- a/.vibe/skills/impeccable/reference/polish.md +++ b/.vibe/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. diff --git a/plugin/skills/impeccable/reference/polish.md b/plugin/skills/impeccable/reference/polish.md index a23c3887f..9db878697 100644 --- a/plugin/skills/impeccable/reference/polish.md +++ b/plugin/skills/impeccable/reference/polish.md @@ -19,7 +19,7 @@ Fix the cause at the narrowest correct level. Ask when a binding system principl ## 2. Gather the evidence -Use the feature yourself at representative desktop and mobile sizes. Determine: +Use the feature yourself at the surface's representative sizes: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes on the simulator, emulator, or hardware, captured per the platform reference's Verifying the build section. Determine: - whether the path is functionally complete; - the intended quality bar and time available; @@ -86,10 +86,10 @@ Do not perfect one corner while leaving the rest below the same quality bar. Walk the complete path again with mouse, keyboard, and touch where applicable. Check: -- mobile, intermediate, and wide layouts; +- mobile, intermediate, and wide layouts on the web; phone and tablet size classes in both supported orientations on native; - loading, empty, error, success, disabled, long-content, and missing-content states; - zoom, contrast, focus, semantics, and screen-reader names; -- console errors, layout shift, interaction latency, image loading, and supported browsers; +- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native; - agreement with DESIGN.md, neighboring features, and the user's scope. Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment. From ab9a29728b0247d8b0c5e5132656c76d79e3d72e Mon Sep 17 00:00:00 2001 From: Abarnaa Sree N Date: Mon, 10 Aug 2026 04:18:07 +0530 Subject: [PATCH 35/35] Warn when static HTML parser dependencies are unavailable (#465) --- cli/engine/engines/static-html/detect-html.mjs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/cli/engine/engines/static-html/detect-html.mjs b/cli/engine/engines/static-html/detect-html.mjs index 482ba0cc3..e08589440 100644 --- a/cli/engine/engines/static-html/detect-html.mjs +++ b/cli/engine/engines/static-html/detect-html.mjs @@ -138,10 +138,21 @@ async function detectHtml(filePath, options = {}) { domutils, }; }); - } catch { - return detectText(html, filePath, options); + } catch (err) { + if (!globalThis.__impeccableStaticHtmlWarned) { + globalThis.__impeccableStaticHtmlWarned = true; + + process.stderr.write( + 'impeccable detect: DEGRADED - HTML parser modules unavailable ' + + '(htmlparser2, css-select, css-tree, domutils).\n' + + 'Falling back to regex matching. Custom properties, selector matching and computed ' + + 'contrast are NOT evaluated; findings are an undercount, not a clean bill of health.\n' +); } + return detectText(html, filePath, options); +} + const resolvedPath = path.resolve(filePath); const fileDir = path.dirname(resolvedPath); const root = profileStep(profile, {