Compare commits

..
Author SHA1 Message Date
github-actions[bot] 0a4e72a254 Sync generated provider output 2026-09-15 00:46:19 +00:00
71a3341289 bake: the anchor must have matched one element on the page
The lasting rules a bake appends apply to every element the anchor
matches, so a `tag.class` anchor shared by siblings (three cards from one
JSX element, say) restyled all of them, not the element the user picked.
The overlay now journals, with the generate event, the anchor it would
bake on (`element.anchor`: the id, else the tag with its classes) and how
many elements matched it when Go fired (`element.anchorMatches`). The
planner bakes only when the source anchor is that same selector and the
count is one; otherwise it leaves the carbonize block with the count in
`bakeSkipped`, and the agent integrates the variant by hand.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
0166cbb870 tests: the agent-target suite arms the live-server reaper
The suite spawned its live servers directly and stopped them in after()
hooks only, so a run killed mid-test could leave them behind. It now arms
the shared reaper and tracks each child like the other live suites do.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
2d2009d7c1 live-server: a departed overlay's report is not a roll-call word
A claim carrying a clientId no connection holds any more (the page
unloaded between the broadcast and the claim landing) was still recorded,
so a departed tab's busy or no_match report could complete the roll call,
or set its verdict, against the overlays that remain. Such a report is
now answered `{granted:false, pending:true}` and not recorded, while any
connection that sent no clientId keeps every id counted as connected.
Eligible claims are left as they were: a lease a departed page holds
lapses and a rescuer takes it, and refusing them would also refuse the
renew a live overlay sends inside an EventSource reconnect gap.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
2f4d4fb4fc live-generate: the dev-server watch answers https and every address
The liveness probe behind `dev_server_gone` only knew `http://` and tried
the first address a host resolved to, so an https dev URL, or a
`localhost` that resolves to ::1 ahead of the 127.0.0.1 the server listens
on, counted as two misses and ended the wait while the server was up. The
probe now takes either scheme (a TCP connect is enough to know a server
is there, TLS or not) and tries every resolved address; the boot's tag
probe shares the address walk and stays plain http, since reading the
document over https would need TLS.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
0603f54a27 bake: the universal selector adds nothing to the anchor
`:scope > *` and `:scope > *.card` describe the accepted element like any
other child compound, but `*` was carried into the merged selector and
produced `div.pricing-grid*`, which no browser matches. The universal
type is dropped on merge, so those rules land on the anchor itself.
Pinned in the rewrite tests.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
4b3a0e932d bake: a :scope child compound merges into the element anchor
`:scope > .card` describes the wrapper's only child, the accepted element
itself, so the lasting rule is the anchor with what the compound adds
(a class the anchor lacks, an attribute, a state), never bare `.card`,
which after the append would style every card on the page. A type in the
compound must be the anchor's own; an id anchor takes any. Pinned in the
rewrite and accept tests; contract updated.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
695d1bd515 generate: plan, tune, and accept exactly like live
The lane's variants were tamer than the ones a live session makes on the
same element: its poll instructions replaced live.md's planning method
with a cheat sheet, its reference forbade knobs, told the agent to copy
the markup verbatim and to treat DESIGN.md as a hard boundary, and its
accept appended anchored overrides instead of integrating the design.
Measured on the same page with Opus, live runs promoted a tier, broke the
grid, and declared knobs; lane runs restyled three equal boxes.

Now a Go the generate verb fires gets the same _instructions as a user's
Go (the action's reference, section 4 planning, knobs per section 7),
generate.md hands the design work to live.md's Handle generate and its
Required after accept, Setup runs as for any command, the Tune chip
behaves as in any session, and the mechanical bake is opt-in (--bake)
instead of the lane's default. The start verdict points at live.md, and
`browser` (the config key the opener reads) is a recognized key.
Goldens re-recorded for the accept help and the recognized-keys line.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
c36e37808e live-generate: stop waiting when the dev server dies
A Cursor run reused a dev server another chat had started; that chat's
terminal was reaped mid-session, so the page never reloaded into the
overlay and --wait-for-browser ran out its 60 s budget before the agent
found an error page and restarted the server by hand (about three
minutes lost). The wait now watches the dev URL it knows (the one it
opened, else the boot's, else the caller's hint) with a TCP connect every
third tick; two misses in a row end it with dev_server_gone, whose
instructions name the harness's way to start the dev script and rerun
with --dev-url. generate.md lists the verdict; an integration test kills
a stand-in server mid-wait and sees the verdict inside seconds.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
04eaefcf00 bake: a component root refuses the bake
A JSX component root (<PricingGrid className="pricing-grid">, <Card.Root>)
renders whatever it likes, and its className or id prop may never reach
that element, so anchoring rules on the prop could persist CSS that
matches nothing or the wrong nested elements while accept reports success.
The bake now refuses such a root (carbonize fallback, bakeSkipped names
the component); only a lowercase element name anchors, custom elements
included. Pinned in the anchor and plan unit tests; contract updated.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
61816b7fbb bake: a component root anchors on its classes, never on its name
A JSX root such as <PricingGrid className="pricing-grid"> is no element
in the rendered page, so the anchor is now the classes alone (or the id);
only a lowercase name is used as a type selector, custom elements
included. A component root with neither refuses the bake as before.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
6b0e68d89d bake: report the stylesheet path in posix form
The accept's `css.file` is a path the agent reads back, so it is
normalised like the wrap step's files; the stylesheet-search test compares
posix paths too. Both showed up on the Windows job as backslashes.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
3f95a12aa8 tests: keep the sh-scripted lane tests off Windows
The cold-project test boots through the CLI, whose detached helper
inherits the test's stdout pipe on Windows and keeps output() from ever
returning; it and the opener-guard section also stand a sh script in for
the browser. Both run on unix only now; the verdict checks that need no
script stay on every platform.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
df4740bad9 review: holder-only generate events, wrapper states and breakpoints in the bake
A generate event may open a session only from the page that holds the
target's lease, or held it last while its lease lapsed or its page went
away; a page that never claimed, or a lapsed holder once a rescuer has
claimed, is refused as before. The pending target remembers its last
holder for that.

The bake now lands a state written on the wrapper (`:scope:hover > .x`,
`:scope[open] > .x`) on the element that takes the wrapper's place, and
rewrites Astro's prefixed rules wherever they sit, `@media` and
`@supports` blocks at the top level included, instead of dropping the
variant's breakpoints.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
899b7afcc9 generate.md: reuse the harness's page, ask when the wording has no direction
The lane is now one page open, three commands and one edit: find the dev
server the harness already runs, open it in the harness's own browser,
start with `--dev-url --boot --wait-for-browser`, edit once, reply and
wait with `--then-poll`, and let the helper bake the accept. A wording
with no direction ("better", "improve") asks one vocabulary question
before anything starts. DESIGN.md is a boundary for the variants, not a
mood board. Knobs are out of the lane. Setup step 1 exempts `generate`,
whose start command loads the same context. Reviewed against
writing-for-agents and the house reference style: a search miss never
starts a second dev server, one move per branch, a checkable done line.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
496b386504 overlay: a lane session shows the variant bar and nothing else
The generate lane hides the helper's global bar, but the overlay drew it
first and hid it on `connected`, the Tune chip spun for the seconds
between the variants mounting and the done reply even though the lane
declares no knobs, and the agent-target pick rendered the edit-copy
pencil with its "disabled while applying" tooltip. The served script's
prelude now says when the bar is hidden so it is never drawn; a session
records who fired its Go (`sessionOrigin`, saved with the session) and
never shows the pending Tune chip when the generate verb did; the
agent-target pick suppresses the edit-copy badge for the session. A
user's own pick, Go, and session are unchanged, and the source test pins
every gate.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
e06c152ad2 generate lane: one-shot start, event in hand, mechanical bake
The generate command's fast lane spent most of its time on agent round
trips, not on the engine. Four engine changes take them out, all behind
the lane's own flags so a plain `live` session is untouched:

- `live-poll --reply <id> done --then-poll` replies and waits for the
  next event in one call; the reply's ack rides along as `_replyAck`.
- `live-generate` collects the session's own generate event into its
  output (`GET /poll?types=generate&id=<sessionId>`, a new id filter
  the parked-poll flush honours too), so the pickup poll is gone.
- `live-generate --boot` runs the lane's boot in-process and reuses a
  running helper; `--dev-url <url>` names the dev server the agent
  already knows and leads the probe; with no page connected the verdict
  is `browser_needed` with the harness's own way to open the page
  (Cursor browser_navigate, Claude Code's Browser pane, Codex --open or
  the user). `--open` launches the system browser only on a harness
  without one: on cursor and claude-code it is ignored unless
  IMPECCABLE_BROWSER or the config's `browser` names a browser, so a
  second window never opens beside the harness's. The served /live.js
  carries the helper-wide bar preference in its prelude.
- The accept of a session the generate verb started (journaled with
  origin "agent", or `--bake`) is baked mechanically: the accepted
  variant's @scope rules are re-anchored on the element's own selector
  and appended to the stylesheet that names it, the wrapper is
  unwrapped, the source verified clean. Knobs, plumbing inside the
  variant, no stylesheet, or a selector the rewrite cannot decide fall
  back to the carbonize block with `bakeSkipped`. `--no-bake` refuses.

Goldens re-recorded for the three help texts and the no-browser case.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
4de812f558 generate.md: knob schema inline, container example, bake knob values
Two Opus runs that asked for tune knobs had to open live.md for the
data-impeccable-params schema the lane names but did not define; the
schema and the CSS it is authored against are in Step 4 now, with the
bake step keeping the chosen branch and substituting range literals.
The Step 3 example targets the container's class instead of the
section id (the preview mounts one copy of the element per variant, so
an id would repeat), and Step 5 says the close replaces the accept
event's own "poll again".

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
65c8089182 Only the lease holder may answer an agent target; leave a plain bar alone
Review found that /agent-target-result checked the shared helper token
and nothing else, so any connected overlay could resolve a target it
never claimed. A result post now names the overlay (`clientId`), and
while the target is pending only its lease holder's word lands: a
bystander gets 409 (not_holder, or unclaimed when nobody holds it) and
the request stays pending. The overlay sends its client id with every
result. Every protocol case now answers from the tab that actually
holds the claim.

Also: the helper-wide bar preference is applied on every connected
frame, and restoring wrote an empty display value, which dropped the
bar's own inline flex layout for a plain live session that never asked
for anything. Hiding remembers the bar's display value, restoring puts
exactly that back, and a restore on a visible bar is a no-op. A plain
boot through the launcher keeps display: flex after connect and its
payload carries none of the lane's keys.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
1a699913e4 Make the generate lane's bar preference helper-wide
The maintainer still saw the bottom bar: two tabs were connected to the
helper, the tab that won the roll call hid its bar, and the tab on
screen never did. The hide was also applied only at Go, so the wait
before it showed the bar as well.

The helper now owns the preference. `impeccable live --no-live-bar`
posts `/live-bar` right after the helper is up, so the bar never
appears in any tab; an agent target carrying `hideLiveBar` sets the
same flag before the target goes out. The helper broadcasts
`live_bar` to every connected tab, answers `hideLiveBar` on every
`connected` frame (reloads, later tabs) and on `/status`, and the flag
lives as long as the helper. The overlay just follows: no per-tab
memory, no session scoping, the variant controls still show. The same
preference skips the overlay's "No PRODUCT.md found" connect notice,
which sent the user to init inside a lane that runs without context by
design.

Verified in a real Chromium session with two tabs, screenshots at each
stage: idle (bar in both), after Go (bar gone in both), after reloading
both, after the accept during the bake, after a reload after that.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
7c42d0feba Keep the live bar hidden for the helper's lifetime, not the session's
The maintainer still saw the bottom bar on the generate lane: the hide
was released the moment the accepted session ended, which is the start
of the agent's bake, so the bar sat there for the minute until the
helper stopped. The choice now lives in sessionStorage keyed on the
helper token: applied at Go, re-applied by every reload's bar rebuild,
kept through the accept and the bake, and forgotten only when the
helper stops and takes the overlay with it. The next `impeccable live`
boots with the bar again.

Verified in a real Chromium tab: hidden through a reload, the accept,
and a reload after the accept; the helper stop removes it entirely.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
e3a121d081 Generate lane: settle the Tune state without knobs, and hide the live bar on request
Two things the maintainer hit testing the lane.

The Tune chip spun forever after a generation whose variants declared
no knobs (the lane's default). The overlay flips the parameter phase to
pending at Go and only settled it when the wrapper mounted; the page
reloads on the JSX write, the resumed session restores "pending" from
its cache with the variants already mounted, and the agent's done reply
never re-checked. Now the done reply completes the phase once every
variant is mounted, and a resume with a pending state asks the helper's
session record whether that generation already finished. A generation
with no knobs shows no chip; one with knobs shows them.

`live-generate --no-live-bar` (body `hideLiveBar: true`, forwarded on
the agent_target payload) keeps the helper's global bar hidden for the
session it starts; the variant controls still show, the choice survives
a reload through the session cache, and the bar returns the moment that
session ends on any path. generate.md passes the flag.

Verified in a real Chromium tab: no chip before and after a reload, bar
hidden through the reload, bar back after the accept. Rust and protocol
cases for the flag, a CLI parse test, contract pins for both fixes.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
9cbe639101 Tests: size the live-server sweep's ps buffer for a busy machine
The orphan reaper and every harness cleanup find live servers through
`ps -A -E`, run with spawnSync's default 1 MiB maxBuffer. A laptop with a
thousand processes produces more than that, spawnSync kills ps, reports
status null, and the sweep silently finds nothing: the SIGKILL leak guard
fails and, worse, real orphans survive. Seen on the maintainer's machine
mid-session, reproduced with main's own engine. A 256 MiB buffer covers
any realistic process table.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
f29636a3d5 Keep the generate lane's boot extras behind flags
The speed pass had every `impeccable live` boot probe for the dev
server and print three new keys (devUrl, contextMissing, contextNote),
which moved an oracle golden and cost a plain live session a probe it
never asked for. The lane's extras are opt-in now: `--dev-url` runs the
probe and reports devUrl; `--allow-missing-context` reports the context
keys. Without either flag the boot's work and payload are byte-identical
to before, which the restored golden and a new boot test pin.

generate.md passes both flags; the contract doc says so.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
1220f26d08 Make the generate lane snappy: self-contained playbook, fast-path poll instructions
The maintainer's field run took five and a half minutes from the prompt
to variants on screen. Two baseline runs on the same repo reproduced it
(356 s mean): 68 KB of skill text read before the first variant (a 36 KB
live.md among it), six to ten tool calls spent finding the dev URL and
the selector, 9 to 10 KB of variants carrying tune knobs, and a document
read plus a detect pass after the accept.

generate.md is now the whole contract for the lane and never sends the
agent to live.md, craft-floor.md, or the action reference on the happy
path; the floors are inlined. The engine carries the rest: a generate
started by live-generate is journaled and queued with origin "agent",
and its poll instructions hand out the fast path (identity from the
event's computed styles and custom properties, the action's three
dimensions, no knobs unless asked, one edit, reply done) instead of the
interactive planning pointer. `impeccable live --allow-missing-context`
boots without PRODUCT.md or DESIGN.md, naming what is missing, so the
lane never falls into the init interview; the boot also reports devUrl,
the origin whose page carries the injected tag, so the agent opens the
page instead of reading terminals. Accept is a bake and live-complete is
its verification: no detect pass, no document read.

Three trimmed runs (one without any context files) averaged 179 s from
prompt to variants, 21 tool calls and 106k tokens against the baseline's
356 s, 30 tool calls and 144k tokens; the accept bake went from 67 s to
41 s. Method and numbers: tmp/questionaire/plan41-field-tests/SNAPPY-REPORT.md
in the maintainer's checkout.

Tests: dev_url probe unit tests, a fast-path instructions unit test, the
origin marker in the protocol suite, and tests/live-boot-fastpath.test.mjs
(flag, contextMissing, devUrl through a stand-in dev server); contract doc
updated.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
d579ecb2f2 Address review: a Go for a target the helper does not hold is refused
The bounded record of answered targets evicted its oldest entry, and a
generate event naming an unrecognized target was admitted, so a Go
delayed past enough later resolutions could still open a session for
a request the CLI had reported as failed.

The admission rule is now positive: a generate event naming an agent
target is welcome only while that target is pending without a rival
lease, or when it comes from the session that answered it. Unknown
targets, evicted or never issued, are refused like any other superseded
Go, so eviction can never reopen a request. The record keeps 256
entries for the answering session's sake.

Tests: a Rust integration case and a Node protocol case (an envelope
naming an unheld target is refused and journals nothing; the same event
without an envelope is an ordinary Go); contract doc updated.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
76db418212 Address review: every answered agent target fences a late Go
Only targets answered with a session were fenced against a delayed
generate event. A request that timed out (or ended in another failure
verdict the CLI already reported) was simply forgotten, so a Go whose
capture outlasted the timeout still opened a session nobody was told
about.

`resolve_agent_target` now records every terminal resolution, with the
answering session when the verdict carried one, and
`agent_target_refusal` refuses a generate event for any answered
target unless it comes from the answering session itself. The
browser_timeout instructions no longer send the agent to live-status
for a session that can no longer start. The overlay's refusal toast
covers both causes.

Tests: a Rust integration case and a Node protocol case (claim, time
out, late Go refused with 409 and nothing journaled), a unit test for
the timeout instruction; contract doc updated.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
7adb81672d Address review: a superseded Go never opens a second session
An overlay renews its lease right before Go, then captures and uploads
the element before its generate event leaves. When that outlasts the
lease and its result post is lost, a rescuer can claim the target and
Go, and the helper accepted both generate events: two sessions for one
request.

The generate envelope now carries this page's clientId, and the helper
refuses a generate event for a target that another page holds under a
live lease, or that was already answered with a different session
(`served_agent_targets`, recorded on every ok resolution): 409
`agent_target_already_served`, nothing journaled. The overlay treats
that refusal like a foreign session and hands the surface back. The
answering session's own event stays welcome, so the common path (result
post first, then the event) is unchanged.

Tests: two Rust integration cases (rival lease, answered elsewhere,
welcome for the serving session) and a Node protocol case, contract
pins for the envelope and the refusal handling, contract doc.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
b5210471fb Address review: the generate event resolves the agent target it serves
A winning overlay could reload after handleGo() minted a session but
before its result post landed. The close released its lease, the
server replayed the still-pending target, and another tab (or the
reloaded page, once it abandoned the unknown session) could claim it
and fire a second Go for a request that already had a session.

The overlay now names the target on the generate event it fires for it
(`agentTarget: {targetId, result}`, the same result it posts), and the
helper resolves the pending request the moment that event is accepted,
stripping the envelope before journaling. Whichever of the event and
the result post lands first answers; a page that dies between Go and
its result cannot leave the request pending, and a request whose event
never reached the helper is served exactly once by the rescuer.

Tests: a Rust integration case and a Node protocol case (claim, Go
event without a result post, verdict carries the session, a late claim
finds nothing pending, the journal carries no envelope), contract pins
for the handoff, and the contract doc.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
335945525d Address review: a done target stays off-limits for a replay
Marking a target done on reply opened a window: an EventSource reconnect
replays the still-pending target while the result is on the wire, the
tab is GENERATING by then, so it declined busy, the server handed the
lease back mid-resolution, and another tab could claim and fire a second
Go. `agentTargetTaken` now covers both acting and done, so a replay of a
target this page took a lease on is ignored, and the late-mount watch
stops on either. Contract pins for the guard and the watch.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
a3bb21cbde Address review: one Go per tab, declines for a granted miss, and grace per overlay
Four review threads on the agent-target protocol and the hook stand-down.

Overlay: a tab acting on one target is busy for every other target
(`agent_target_in_flight`), so two held generate requests can never both
be claimed by one tab and the second Go can never overwrite the session
the first one minted. Every exit from actOnAgentTarget ends the acting
state, and teardown clears the target ledger, so a Go that never happened
does not refuse the next connection's targets. A miss after a granted
claim now declines (handing the lease back so another page or a remount
can serve) instead of posting a result that ended the request for every
tab.

Hook: the live-preview marker probe runs before the per-session edit cap,
so a file already past the cap stands down for a variants wrap instead
of emitting the suppression notice.

Server: each overlay's first no_match word extends the resolution grace
by the full window (its watch re-reports do not), so an overlay that
reports after another page's grace lapsed still gets its late-mount
watch instead of completing the roll call with a no_match verdict.

Tests: a Rust and a Node protocol case for the late overlay's grace, a
hook case for the cap-then-wrap order, and contract pins for the busy
check, the decline on a granted miss, and the teardown clear.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
79a27051a2 Address review: hold an all-no_match roll call open for a resolution grace
When the unresolvable page's decline was the last word, the roll call
completed on it, the answer said pending:false, and the page's watcher
never started, so an element that mounted a moment later was still
answered no_match. A page's no_match is a provisional word: the server now
keeps an all-no_match roll call open for IMPECCABLE_AGENT_TARGET_RESOLVE_GRACE_MS
(default 3000) after the first such report, re-judging when the grace
lapses, so a page that keeps watching can still claim (the stale report is
dropped on its eligible claim); a busy report still answers at once. The
overlay reports a miss immediately and re-checks every half second for as
long as the answer says pending. A genuine no_match now takes about the
grace instead of tens of milliseconds, inside the server's hold.

Rust integration case for the late mount claiming within the grace, the
protocol case, and the contract assertions updated; the contract documents
the grace and its env override.

AI-assisted: implemented and tested with Claude Code under maintainer
direction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
bd4ec3a11c Address review: the server ends the resolution watch, and a reconnect re-participates
Two ways a page's word could go stale after the resolve-before-claim
change: an element that mounts later than the quick re-checks, and an
EventSource reconnect that did not overlap the old connection (the server
drops that page's word on the close, replays the target, and the replay
guard ignored it, so the roll call waited on a word that never came).

A decline's answer now carries pending, like a denied claim does, so a page
that could not resolve the target reports the miss after the quick
re-checks (the roll call can complete on the other overlays' words) and
keeps re-checking once a second for as long as the server says the request
is pending, claiming the moment the element mounts; the server drops the
stale report on an eligible claim and ends the watch by answering
pending:false once the request resolved or timed out. The overlay tracks
its participation per target: a replayed target is ignored only while this
page is acting on it, and is otherwise handled again, so a busy or
unresolvable page re-declines (idempotent) and an idle page claims.

Unit, protocol, and contract cases updated; the decline answers now say
whether the request is still pending.

AI-assisted: implemented and tested with Claude Code under maintainer
direction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
46f1dd6b36 Address review: re-check a failed resolution before declining an agent target
A page's element can be momentarily absent (a route still rendering, an
HMR commit mid-swap), so a failed resolution is not that page's final
word. The overlay now re-checks at 300, 700, and 1500 ms, claims the moment
the element mounts (the server already drops the stale report on an
eligible claim), and reports only the last miss. A genuine no_match now
takes about two seconds instead of tens of milliseconds, well inside the
server's hold.

Also normalizes a path separator in the new hook unit test, which failed
on rust-windows because the audit's file path carries backslashes there.

AI-assisted: implemented and tested with Claude Code under maintainer
direction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
25263adc51 Roll call: a page that cannot resolve the target declines instead of claiming
Field-testing with two pages open showed the first-wins claim letting the
wrong page answer: a tab whose page lacks the element won the claim,
resolved the selector locally, and replied no_match while another page had
the element. The overlay now resolves the selector before any claim and,
when its page cannot resolve it, declines with reason no_match and the
resolution verdict; the same check runs on the busy-to-idle re-claim. The
server records that verdict on the report and, once every connected
overlay has declined, prefers a report that could serve later (a tab
mid-session or with an apply in flight, which answers busy so the agent
retries) over no_match, and returns the resolution verdict only when no
page can serve; the timeout uses the same precedence.

Also from the same field tests: a tab on another page of the app was
resuming this page's session from the per-origin localStorage cache after
a dev-server reload re-initialised it, then sat in GENERATING for a wrapper
it never renders and declined every later target. restoreSessionWithoutWrapper
now resumes a cached session only on the page that saved it, the check the
server-adoption branch beside it already applied.

Covered by two Rust integration cases, two protocol cases, and contract
assertions for the resolve-before-claim path and the page gate; the
cross-page scenario of the field harness passes on a two-page site.

AI-assisted: found by field tests and fixed with Claude Code under
maintainer direction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
8fb7f7fec5 Hook: stand down for the whole edit when the primary carries live markers
Field-testing the generate command on a Vite React app showed the
per-file stand-down was not enough: an edit to the wrapped App.jsx skipped
that file but still co-scanned the stylesheet it imports and spoke up
about it (a clean ack or findings) mid-session, which is exactly the noise
the stand-down exists to prevent. When the edited primary file carries the
markers, the whole PostToolUse event now returns skipped: live-preview
with the audit naming that primary, co-scanned stylesheets included; a
marked file that is only co-scanned still skips alone. A unit case covers
both, and the contract documents the event-level stand-down.

AI-assisted: found by field tests and fixed with Claude Code under
maintainer direction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
da403a3410 Address review: the overlay, not the connection, is the roll-call participant
An EventSource reconnect opens a replacement connection under the same
page-level clientId before the old connection is seen to close, so the
close handler used to retire the reconnected overlay's report and hand
its lease back mid-flight. remove_sse_client now retires a client's word
only when no other connection still carries its id, the roll call counts
distinct overlays (plus id-less connections) instead of raw connections,
and the overlay ignores a replayed target it already handled, so a
reconnect never starts a second claim or a second Go. Covered by two new
HTTP cases in crates/cli/tests/agent_target.rs, a protocol case in
tests/live-agent-target.test.mjs, and the overlay contract suite.

AI-assisted: implemented and tested with Claude Code under maintainer
direction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
d397140a77 Port /impeccable generate to the engine crates
The Node-era server, CLI, hook, and pin halves of the generate command move
into the Rust workspace, with the protocol unchanged:

- crates/live: POST /agent-target is held open on a channel plus a timer
  thread (the manual-apply deferred pattern), releasing its turnstile
  ticket before it parks like /poll; /agent-target-result resolves it;
  /agent-target-claim is the roll call with its renewable lease. SSE
  connections carry the overlay's clientId: a late overlay is replayed
  every pending target, and a disconnect retires that overlay's report,
  releases its lease, and re-judges each roll call. Shutdown drains held
  requests with server_stopping.
- crates/live/src/live_generate.rs: the live-generate verb (the router
  already forwards every live* verb), same flags, verdicts, and
  _instructions, spelled with the engine's self command.
- crates/hook: every entry stands down on live preview markers
  (skipped: live-preview), checking the proposed content and the file on
  disk for hook-before-edit.
- crates/context: pin accepts generate; the crate's command-metadata.json
  copy carries its entry.

Tests: crates/cli/tests/agent_target.rs (six HTTP cases with an SSE reader),
tests/live-agent-target.test.mjs rewritten to drive the binary (28 cases,
registered in the live suite), hook stand-down cases, oracle goldens for
live-generate plus the re-recorded pin list goldens, the e2e prompt
assertion waiting for the journaled event, and the contract documented in
docs/CLI-CONTRACT.md.

AI-assisted: implemented and tested with Claude Code under maintainer
direction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
fc89b0ed62 Add /impeccable generate: agent-initiated live variants (Node-era squash)
Squash of the ten commits reviewed on PR #626, plus the last review
round's connection-aware roll call, before the rebase onto the Rust
engine: the generate command reference and router row, the overlay's
agent-target handling (roll call, leases, replay, rescue), the Node-era
live-server routes and live-generate CLI, the hook stand-down, the pricing
cards e2e fixture, and the unit, contract, e2e, and skill-behavior tests.
The server, CLI, hook, and pin halves are ported to the engine crates in
the commits that follow.

AI-assisted: implemented and tested with Claude Code under maintainer
direction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00
Paul BakausandGitHub 1c043ea7c9 Simplify URL component escaping (#821)
Replace three duplicated UTF-8 escaping loops with one private encoder and explicit component character sets. Preserve existing URL behavior with a narrow characterization test.

Prepared with AI assistance by Codex under pbakaus’s standing authorization for the daily architecture-simplification automation.
2026-09-14 17:43:44 -07:00
dependabot[bot]andGitHub 2149fcce39 Build(deps-dev): Update Bun dependencies while preserving the AI tool-loop hold (#819)
Retain eight dependency updates and ai 7.0.69 with its private provider stack. Validated frozen install, Rust release build/workspace tests, full default suite including engine oracle, extension/VS Code packaging, new-work browser tests and exact-head GitHub checks.

The additional full live sweep has one inherited orphan-session cleanup failure, reproduced identically on unchanged main and the candidate; 35 other tests pass. Provider-backed behavior remains unverified and the known ai regression hold is preserved.

AI-assisted dependency review, minimal fix, and merge by Codex for the maintainer-authorized sweep.
2026-09-14 10:15:51 -07:00
dependabot[bot]andGitHub 96b37ea538 Build(deps): Bump azure/login from 3.0.2 to 3.1.0 (#820)
Validated the unchanged OIDC inputs, Node 24 action runtime, default client-ID masking, workflow signing boundaries, core tests, distribution build, and exact-head required GitHub checks. The tag-only Azure signing workflow was not executed locally.

AI-assisted dependency review and merge by Codex for the maintainer-authorized sweep.
2026-09-14 10:07:16 -07:00
cb56ed6c19 Fix: detect placeholder contrast (#790) (#799)
* Fix: detect placeholder contrast (#790)

`detect` never read `::placeholder` color, so pale placeholders passed. Score them with the same WCAG math as body text, without host class/clip heuristics.

Prepared with AI assistance.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix: match descendant ::placeholder hosts (#790)

`.form ::placeholder` kept the ancestor as the host. Reuse the hover combinator star-fill so the color lands on the inputs inside.

Prepared with AI assistance.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix: placeholder-shown and gradient alpha (#790)

Browser scans skip when :placeholder-shown is false, so a live filled field does not keep the HTML value attribute's empty state. Translucent placeholders flatten over each gradient stop before scoring.

Prepared with AI assistance.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix: trailing combinator only for ::placeholder hosts (#790)

`star_empty_compounds` turned `.label + ::placeholder` into `.label *+*`. Fill only a trailing empty compound so adjacent-sibling hosts still match.

Prepared with AI assistance.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 13:49:59 -07:00
github-actions[bot] 3e1f67c52c Sync generated provider output 2026-09-10 20:46:13 +00:00
0c09f4c7e2 Fix: verify touch gestures in adapt, audit, and harden (#805) (#807)
* Fix: verify touch gestures in adapt, audit, and harden (#805)

The verification sections of adapt.md, audit.md, and harden.md listed
environments and layout properties but never had the agent exercise a
control's primary gesture, so an emulated viewport plus screenshots
could pass as touch testing. adapt now exercises the primary gesture
and the scroll-across trade and reports what produced the evidence,
audit checks broken touch interaction with code-level tells, harden
covers interrupted gestures and recovery, and a reference-contract
test pins the three sections.

Prepared with AI assistance (Claude Code), directed by @abdulwahabone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Pin the scroll, exercise, and cleanup sentences in the reference test (#805)

Greptile flagged that the contract test pinned the new labels but not
adapt's scroll-across trade, audit's instruction to exercise the
gesture, or harden's drag-state and capture cleanup.

Prepared with AI assistance (Claude Code), directed by @abdulwahabone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 13:45:37 -07:00
7a7579858c test: scenario 19 documentation case when the context launcher is denied (#791)
* Add degraded Setup path: must-read pack when the context launcher is refused

When the host denies the impeccable context launcher (issue #789, measured
in #744), the Setup fallback now names the degraded path and its
unconditional must-read pack: the routed command's reference and
craft-floor.md before any UI edit, and document.md before writing DESIGN.md.
init.md gains the degraded Step 1 behavior, docs/CLI-CONTRACT.md documents
the degraded contract, and scenario 19 gains a denied-launcher documentation
case asserting document.md and source reads precede the DESIGN.md write.

No version bump, no changelog entry, no generated harness sync.

AI was used for assistance.
Includes AI_PR_NOTICE.txt per the repository's contribution policy: this
change was prepared without maintainer approval on issue #789, so no PR is
opened by the agent.

Co-authored-by: Matt Van Horn <mvanhorn@users.noreply.github.com>

* Drop restated degraded-setup prose; keep the scenario 19 documentation case

The launcher-unavailable path already lives on main. This removes the
notice file and the restated SKILL, init, and CLI-contract text, and keeps
the denied-launcher documentation coverage. The notice must now land before
the first tool call after the denial, not only before the eventual write.

AI was used for assistance.

Co-authored-by: Matt Van Horn <mvanhorn@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-09-10 13:44:25 -07:00
67d018fe05 Fix: print JSON on live-poll --reply success (#800)
Successful --reply was exit 0 with empty stdout, so agents could not tell delivery from a hang. Prepared with AI assistance.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 08:47:39 +05:00
3bdb9ff06c Fix: drop stale carbonize diagnostic on complete (#801)
Complete and discarded snapshots no longer keep carbonize_cleanup_required after cleanup is done.

AI assistance: Cursor Grok 4.6.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 08:47:16 +05:00
476 changed files with 21748 additions and 12938 deletions
+3 -2
View File
@@ -1,7 +1,7 @@
--- ---
name: impeccable name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks. description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.4.0 version: 4.3.1
license: Apache 2.0 license: Apache 2.0
allowed-tools: allowed-tools:
- Bash(npx impeccable *) - Bash(npx impeccable *)
@@ -66,7 +66,8 @@ Choose the mode from the requested surface, not the product, and persist it only
| `clarify [target]` | Fix | Improve UX copy, labels, and error messages | [reference/clarify.md](reference/clarify.md) | | `clarify [target]` | Fix | Improve UX copy, labels, and error messages | [reference/clarify.md](reference/clarify.md) |
| `adapt [target]` | Fix | Adapt for different devices and screen sizes | [reference/adapt.md](reference/adapt.md) · native: [reference/adapt.native.md](reference/adapt.native.md) | | `adapt [target]` | Fix | Adapt for different devices and screen sizes | [reference/adapt.md](reference/adapt.md) · native: [reference/adapt.native.md](reference/adapt.native.md) |
| `optimize [target]` | Fix | Diagnose and fix UI performance | [reference/optimize.md](reference/optimize.md) | | `optimize [target]` | Fix | Diagnose and fix UI performance | [reference/optimize.md](reference/optimize.md) |
| `live` | Iterate | Visual variant mode: pick elements in the browser, generate alternatives | [reference/live.md](reference/live.md) | | `live` | Iterate | Visual variant mode: pick elements in the browser, iterate on alternatives | [reference/live.md](reference/live.md) |
| `generate [n] [action] [element]` | Iterate | Variants, versions, or alternatives of a named element to choose from in the live browser; no manual picking | [reference/generate.md](reference/generate.md) |
Routing: Routing:
@@ -188,6 +188,12 @@ Test thoroughly across contexts:
- **Edge cases**: Very small screens (320px), very large screens (4K) - **Edge cases**: Very small screens (320px), very large screens (4K)
- **Slow connections**: Test on throttled network - **Slow connections**: Test on throttled network
**Custom controls** (sliders, drag surfaces, scrollable control strips): a before/after slider can pass every width check above and still refuse to drag on iOS, so exercise each one in scope in the same batched round as the checks above:
- **Primary gesture**: Tap it and confirm it responds as designed, then drag it with the target input method; the drag must complete, not just start
- **Scroll across it**: A swipe along the page's scroll axis across the control scrolls the page or container without activating it; a drag that starts on the control along its axis moves the control, not the page. Neither failure throws an error, so try both
- **Evidence**: Say what produced the evidence: an emulated viewport, synthesized touch input through a browser tool, which engine ran it (Chromium is not Safari), or a physical device. Screenshots and resized viewports verify layout, never a gesture. Name what stayed untested and move on; unreachable hardware is a reported gap, not a blocker
When the adaptation feels native to each context, hand off to `/impeccable polish` for the final pass. When the adaptation feels native to each context, hand off to `/impeccable polish` for the final pass.
--- ---
+2 -1
View File
@@ -48,11 +48,12 @@ Run comprehensive checks across 5 dimensions. Score each dimension 0-4 using the
**Check for**: **Check for**:
- **Fixed widths**: Hard-coded widths that break on mobile - **Fixed widths**: Hard-coded widths that break on mobile
- **Touch targets**: Interactive elements < 44x44px - **Touch targets**: Interactive elements < 44x44px
- **Broken touch interaction**: Custom sliders, drag surfaces, and scrollable control strips whose primary gesture fails under touch, that swallow page scroll or lose the drag to it, or that stay stuck after an interrupted gesture. Code tells: mouse-only handlers, no `touch-action` on a pointer-event drag surface, drag state that nothing clears on cancel, lost capture, or blur. Exercise the gesture when a browser tool can synthesize touch (a rendered viewport proves layout, not the gesture), then say what produced the evidence (emulated viewport, synthesized touch, which engine, physical device) and what stayed untested
- **Horizontal scroll**: Content overflow on narrow viewports - **Horizontal scroll**: Content overflow on narrow viewports
- **Text scaling**: Layouts that break when text size increases - **Text scaling**: Layouts that break when text size increases
- **Missing breakpoints**: No mobile/tablet variants - **Missing breakpoints**: No mobile/tablet variants
**Score 0-4**: 0=Desktop-only (breaks on mobile), 1=Major issues (some breakpoints, many failures), 2=Partial (works on mobile, rough edges), 3=Good (responsive, minor touch target or overflow issues), 4=Excellent (fluid, all viewports, proper touch targets) **Score 0-4**: 0=Desktop-only (breaks on mobile), 1=Major issues (some breakpoints, many failures), 2=Partial (works on mobile, rough edges), 3=Good (responsive, minor touch target or overflow issues), 4=Excellent (fluid, all viewports, proper touch targets, gestures work under touch)
### 5. Implementation Integrity (CRITICAL) ### 5. Implementation Integrity (CRITICAL)
@@ -1,57 +0,0 @@
# Component review
Use this checkpoint on comp-led builds after producing the initial component kit and before composing the page. The approved comp is the reference. The user reviews the actual produced components, including code; a list of planned assets or screenshots supplied by the builder is not a review of what will ship.
## Prepare the component kit
Keep the measured spec's region IDs. Include every visible region: produced raster assets and working HTML/CSS/SVG for text, controls, patterns, decoration and layout elements. A region rendered in code needs an actual review document, not a promise to implement it later. Use semantic HTML for content and controls. Do not flatten the page or combine unrelated regions to avoid review. Report omitted regions so the user can mark what is missing.
Write `.impeccable/review/components.json` with this manifest format:
```json
{
"schemaVersion": 1,
"id": "components",
"title": "Component review",
"stage": "components",
"comp": {"path": ".impeccable/mocks/comp-2.png", "width": 1536, "height": 1024},
"components": [
{
"id": "illustration",
"name": "Illustration",
"medium": "raster",
"box": {"x": 0.5, "y": 0.2, "w": 0.45, "h": 0.7},
"note": "Produced cutout; positioned over the page ground.",
"preview": {"kind": "image", "path": "assets/illustration.png"},
"dependencies": [".impeccable/build/spec.json"]
},
{
"id": "headline",
"name": "Headline",
"medium": "html",
"box": {"x": 0.05, "y": 0.2, "w": 0.4, "h": 0.25},
"note": "Rendered semantic heading and its typography.",
"preview": {"kind": "page", "path": ".impeccable/review/components/headline.html"},
"dependencies": [".impeccable/build/spec.json", "assets/type.woff2"]
}
]
}
```
The coordinates above only illustrate the schema. Use the approved comp's actual pixel dimensions and each measured region's normalized bounds (`x / width`, `y / height`, `w / width`, `h / height`). A code preview is rendered at the comp viewport and cropped to that component's box, so place its content at those coordinates in the review document. Include every file the document uses in `dependencies`, including linked CSS, fonts and images. The runtime also binds the measured spec for the component stage and checks its inventory. Local paths only. Static PNG, WebP and JPEG previews retain their original bytes and actual transparency; never draw a checkerboard into the asset.
Native capture supports stable HTML/CSS and inline SVG. Supply a static review state for motion and keep the implementation's real inputs. A scripted, canvas or otherwise unsupported component is a blocker to report, not permission to substitute a raster or omit it.
## Present and wait
If the harness exposes `component_review`, call it with `manifest_path` set to `.impeccable/review/components.json`. The host captures the component files, presents this same review interface and returns the user's decisions. A suspended request is waiting for the user; it is not a failed build or an approval.
Otherwise run `.agent/skills/impeccable/scripts/impeccable component-review capture --manifest .impeccable/review/components.json`, then start `.agent/skills/impeccable/scripts/impeccable component-review serve --session <returned session>` in the background. Open the URL it prints in the available browser and wait for the user. Read the result with `.agent/skills/impeccable/scripts/impeccable component-review verify --manifest .impeccable/review/components.json`; pending, needs-work and stale input all refuse approval. Never submit the page or write a receipt on the user's behalf.
The user can approve components, request changes, and mark missing regions. Act on their feedback without replacing it with your own favorable verdict. Keep component IDs stable, update the actual implementation and dependency list, and present another round. The UI carries only approvals whose component inputs have not changed. Do not ask the user to reapprove unchanged work. Continue only when the inventory is confirmed and all components are approved.
## Assemble and review
Build the page from the approved component files. Replacing, simplifying or changing an approved component requires a new component review. Run the existing plates and hero gates; human review does not waive their integrity checks.
After the full page and responsive checks are complete, present a second manifest at `.impeccable/review/hero.json`, with `id` and `stage` set to `hero`. Use one page-preview component covering the assembled first viewport, its real HTML entry, and its complete dependency list. The reference stays the approved comp. Call the same host review tool (or native capture/serve/verify workflow) and obtain the user's approval before the final response. Later edits to the reviewed files require a fresh review. A component-kit approval does not approve their assembled layout.
@@ -13,10 +13,6 @@ Do not redesign. Preserve the reference's visual role, silhouette, palette, ligh
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 `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; 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; report a card too thin to brief a comp, never pad it 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 (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 keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. 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. 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 `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; 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; report a card too thin to brief a comp, never pad it 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 (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 keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. 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.
## Review handoff
After the initial production batch, return the actual files and any unresolved drift to the parent for the user-facing component review in [component-review.md](../reference/component-review.md). The parent includes rendered code regions alongside these rasters. A parent or automatic visual check is not a substitute for that human checkpoint. Apply requested repairs and preserve unchanged assets; do not self-approve them. This checkpoint does not apply to the Decision Comps job above.
## Input Contract ## Input Contract
Expect the measured spec (`.impeccable/build/spec.json`, written by `impeccable comp-spec` from the approved comp), the approved comp path, and the skill scripts path. Optionally: a subset of region ids to produce, extra prompt notes per region, and format or transparency needs. Everything else you need is in the spec: each raster region's id, kind (plate, image, texture), pixel box, sampled palette, aspect, note, and the plate path it must land on. Expect the measured spec (`.impeccable/build/spec.json`, written by `impeccable comp-spec` from the approved comp), the approved comp path, and the skill scripts path. Optionally: a subset of region ids to produce, extra prompt notes per region, and format or transparency needs. Everything else you need is in the spec: each raster region's id, kind (plate, image, texture), pixel box, sampled palette, aspect, note, and the plate path it must land on.
@@ -16,7 +16,7 @@ Expect: the original request; the confirmed user answers; the artifact path(s);
## Checks, in order ## Checks, in order
0. **Evidence.** Before any other check, verify the required captures exist and every capture is valid. Required: the platform's full viewport set (web: `desktop.png` and `mobile.png`; native: one capture per shipped device class), plus every capture the calling brief names as required, a reported user viewport (`user-<width>.png`) included. Valid: no black or blank regions, content matching what the filename claims (a visit capture showing the About section is invalid), the document top visible where the file claims a full page, dimensions that make sense for the named viewport. A required capture that is absent fails exactly like one that is malformed: a viewport nobody captured is a viewport nobody inspected, and it cannot ship. When any capture fails, the whole review changes shape: return `disposition: recapture` as the first line, then one section, `recapture`, listing each missing or invalid file and what a valid capture of it shows, and stop. Never build a matrix on malformed evidence; a verdict derived from a broken capture launders the breakage into an approval, and the parent owes you a full re-review on valid captures, not a scoring round. 0. **Evidence.** Before any other check, verify the required captures exist and every capture is valid. Required: the platform's full viewport set (web: `desktop.png` and `mobile.png`; native: one capture per shipped device class), plus every capture the calling brief names as required, a reported user viewport (`user-<width>.png`) included. Valid: no black or blank regions, content matching what the filename claims (a visit capture showing the About section is invalid), the document top visible where the file claims a full page, dimensions that make sense for the named viewport. A required capture that is absent fails exactly like one that is malformed: a viewport nobody captured is a viewport nobody inspected, and it cannot ship. When any capture fails, the whole review changes shape: return `disposition: recapture` as the first line, then one section, `recapture`, listing each missing or invalid file and what a valid capture of it shows, and stop. Never build a matrix on malformed evidence; a verdict derived from a broken capture launders the breakage into an approval, and the parent owes you a full re-review on valid captures, not a scoring round.
1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and every phase before `review` is `closed` or explicitly `skipped`; an open or failed phase is a material finding. A comp-led config with no state file, or a state whose `comps` phase is neither `closed` nor `skipped`, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. 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, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all. 1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and its `comps` (or `skipped` when a surface round locked the comp), `spec`, `plates`, and `hero` phases are `closed`; a comp-led config with no state file, or a state whose `comps` phase never closed, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. 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, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all.
2. **Fidelity.** Start from the measurement, then judge what it cannot: read `.impeccable/review/diff/final/report.json` (and hero) first; every region scored `missing` or `contradicted` is a matrix row in that state unless the paired crop under `regions/` shows the score is wrong, and you say why; a region scored `match` still gets your eye for lettering character and material, which the numbers do not measure. Then, 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; 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. Three 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; medium is part of the promise. GROUND: the page field's value and temperature against the comp's, sampled from pixels on both sides when tooling allows rather than judged from memory, and read as the net on-screen result where a texture or tile paints over the base color; a ground warmer or cooler than the comp's is contradicted however faithfully the layout matches, and drift toward the rendition prior (warm cream on light grounds, blue-black slate on dark) is the direction to hunt. With no approved comp, 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 renders) as contradicted on its face; imitation material is the single most reliable mark of machine-made design. GROUND narrows rather than lapses: with no comp to sample, a color OWN-WORLD names is the target and the same warmer-or-cooler judgment applies; when OWN-WORLD names none, there is no GROUND authority, and the review says so in place of a verdict, because a target the reviewer invents turns the check into taste. A critique-reference comp on such a build is provocation, not spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is what the image dared that the build did not, and 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. A fix that requires producing an asset says so explicitly ("produce: <region> 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. 2. **Fidelity.** Start from the measurement, then judge what it cannot: read `.impeccable/review/diff/final/report.json` (and hero) first; every region scored `missing` or `contradicted` is a matrix row in that state unless the paired crop under `regions/` shows the score is wrong, and you say why; a region scored `match` still gets your eye for lettering character and material, which the numbers do not measure. Then, 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; 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. Three 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; medium is part of the promise. GROUND: the page field's value and temperature against the comp's, sampled from pixels on both sides when tooling allows rather than judged from memory, and read as the net on-screen result where a texture or tile paints over the base color; a ground warmer or cooler than the comp's is contradicted however faithfully the layout matches, and drift toward the rendition prior (warm cream on light grounds, blue-black slate on dark) is the direction to hunt. With no approved comp, 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 renders) as contradicted on its face; imitation material is the single most reliable mark of machine-made design. GROUND narrows rather than lapses: with no comp to sample, a color OWN-WORLD names is the target and the same warmer-or-cooler judgment applies; when OWN-WORLD names none, there is no GROUND authority, and the review says so in place of a verdict, because a target the reviewer invents turns the check into taste. A critique-reference comp on such a build is provocation, not spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is what the image dared that the build did not, and 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. A fix that requires producing an asset says so explicitly ("produce: <region> 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. 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, 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. 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, 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.
@@ -0,0 +1,101 @@
> **Additional context needed**: only the target element, when the request does not name one that resolves uniquely on the page.
Generate is the fast lane into live mode: the user names an element, a direction, and a count in one sentence, and within a minute they are cycling through variants in their browser. One command boots the helper, hands the element to the overlay in the page your harness already shows (it scrolls to it, selects it, and fires the same Go a click fires) and returns the generate event; one edit writes the variants; one call replies and waits for the user's choice, which the helper bakes into source itself. This file owns the lane's plumbing; from the event onward the design work is [live.md](live.md)'s, unchanged, so read it in full now if you have not this session.
**Web only.** Live mode's browser overlay has no native equivalent; on `ios` / `android` / `adaptive` projects, decline this command and offer `bolder` or `quieter` on the source instead.
The plumbing is where the lane saves time: one command starts the session around the page your harness already shows, one call replies and waits, and nothing here is a browser you have to babysit. The design work is not where it saves time. Setup runs as for any command (`impeccable context`, this reference, craft-floor.md before the edit), and the variants are planned, written, and accepted exactly the way a live session plans, writes, and accepts them.
Three prohibitions cover the known ways this command goes wrong:
- **Never run init or document, and never ask for PRODUCT.md or DESIGN.md.** When they exist, the start command prints them under `boot` and you use them. When they do not, it says so (`contextMissing`, `contextNote`) and you extract the identity from the event (Step 3). A missing file is never a reason to interview the user inside this command; offer `init` in one line after the session ends.
- **Never hand-write a variants wrapper or invent a session id.** Only the browser mints session ids (8 hex characters, at Go). A missing event is fixed by rerunning Step 2, never with a direct source edit.
- **Do not act on hook findings while live markers are in the file**, and do not restyle variants to appease them; the accept verifies the file once the variant is permanent.
## Step 1: Parse the request
Three parts, all from the user's sentence:
- **A number in the request**: that is the count. **No number**: 3. The protocol caps count at 8.
- **The direction wording** maps onto the live action vocabulary; never invent a new action value:
- **bold, bolder, stronger, punchier**: `bolder`
- **quiet, calmer, softer, toned down**: `quieter`
- **simpler, minimal, stripped**: `distill`
- **refined, tightened, polished**: `polish`
- **font and type words**: `typeset`
- **color words**: `colorize`
- **arrangement and spacing words**: `layout`
- **device and breakpoint words**: `adapt`
- **motion words**: `animate`
- **playful words**: `delight`
- **rule-breaking words**: `overdrive`
- **Wording that carries intent but no vocabulary word** ("make it feel like a bank", "warmer", "more premium"): `impeccable`, with the user's wording passed as the prompt.
- **An action fits AND extra intent rides along** ("bolder, but keep it monochrome"): that action, with the rest as the prompt.
- **The wording names no direction at all** ("better", "improve", "nicer", "different", "fresh", "new", "redesign", "fix", "some options", "ideas", "alternatives", or just "variants" with nothing else): Ask the user directly to clarify what you cannot infer. Ask one question, offering the vocabulary: *"Which direction should the variants take? bolder, quieter, simpler (distill), polished, typography (typeset), color (colorize), layout, motion (animate), playful (delight), or rule-breaking (overdrive)."* Map the answer with this list; an answer that is still open ("surprise me", "you pick") is `impeccable` with the user's original wording as the prompt, and Step 2 starts on that answer.
- **The element description** ("the pricing cards", "the hero heading"): Step 2 resolves it to a selector.
Done when you hold an action from the vocabulary (asked for, when the request named no direction), a count from 1 to 8, and the element description.
## Step 2: Reuse the page, then start
**Reuse** the dev server already running and the tab your harness already shows it in; a second server or a second browser window is the failure this step prevents.
1. **Find the dev server**, cheapest source first, and stop at the first hit: the user's message, a browser tab already on the app (Claude Code: an origin in `tabs_context`), a server your harness started (Claude Code: `preview_list`), a terminal that printed its URL. Its origin is your `--dev-url`. **No hit**: leave `--dev-url` off and run the start command with no wait; the boot probes for a running server and its verdict names the move. `browser_needed` carries the `devUrl` it found: open it as in 2, then rerun with `--dev-url <devUrl> --wait-for-browser 60000`. `no_dev_server` means nothing serves the app: start the dev script the way the verdict says (Claude Code: `preview_start`; Cursor: a background terminal; Codex: an exec you yield from), wait for its URL, then rerun with `--dev-url <url>`.
2. **Open the page that renders the element in your browser, then start.** The route the request names, else the one `--target` serves; `--dev-url` takes only the origin.
- **Cursor** (`browser_navigate`) and **Claude Code** (`navigate`, which opens the Browser pane when it is closed and takes the `tabId` from `tabs_context` when a tab is already on that origin): open the URL, then run the start command with `--dev-url <url> --wait-for-browser 60000`. The boot injects the overlay and the page reloads into it while the command waits. Your browser tool is the only opener on these harnesses; the engine ignores `--open` there.
- **No browser tool** (Codex, others): run the start command with `--open --wait-for-browser 120000`; it opens the system browser, and the longer wait covers the user finding the tab. **`browser_open_failed` back**: tell the user the `url` in one line and rerun with `--wait-for-browser 120000`.
```bash
.agent/skills/impeccable/scripts/impeccable live-generate --target src/App.jsx --dev-url http://127.0.0.1:5173/ --selector ".pricing-grid" --action bolder --count 3 --boot --wait-for-browser 60000
```
Run it in the foreground in Cursor and Claude Code (it returns within the wait); on Codex, in an exec you yield from, the way Step 3 runs the poll.
- `--target`: the file that renders the element when the request or the project makes it obvious; skip it otherwise.
- `--dev-url`: the origin from 1; omit it and the boot probes.
- `--selector`: a unique class first, then a landmark tag plus class, an id last (every variant mounts a copy of the element, so an id repeats in the DOM). **The request names a repeated component in plural** ("the pricing cards"): target the container that holds the set, so one scoped stylesheet restyles every instance. One read of the source file that renders the element is allowed when the selector is not obvious; `--dry-run` resolves and reports without starting anything when it is not certain.
- `--boot`: runs the lane's boot (PRODUCT.md and DESIGN.md loaded again for the helper, missing files tolerated, dev URL found, bottom bar hidden for the helper's lifetime) and reuses a helper that is already running. Its result rides along as `boot`.
- Also available: `--prompt`, `--text` (keep only matches whose visible text contains a snippet), `--index` (1-based pick among matches).
Read the output in this order: `boot` (or `boot.contextMissing` with `boot.contextNote`: the page is the source of truth, per the note), then `event`, the generate event for `sessionId`, with the same `_instructions` a user's Go gets. Every verdict carries `_instructions`, and they win over your recollection of this file; the ones whose move is a decision of yours:
- **`ambiguous`**: the candidates are listed; target their common container, or rerun with `--text "<visible text>"` or `--index <n>`.
- **`dev_server_gone`**: the dev server stopped answering while the command waited for the page (on Cursor, a server another chat started dies with that chat). Start it the way the verdict says, then rerun with `--dev-url <url>`.
- **`no_match`**: the tab is on a route that does not render the element (navigate to the right route, rerun), or the selector is wrong (derive a better one from the source, or add `--text`).
- **`config_missing` / `config_invalid`** under `bootError`: follow [live-setup.md](live-setup.md) first, then rerun.
- **`event: null`** with `ok: true`: the event was slower than the wait; run `.agent/skills/impeccable/scripts/impeccable live-poll` once to collect it, then continue.
Done when the output shows `ok: true`, a `sessionId`, and an `event`, reached with at most one server started and one tab opened by you.
## Step 3: Generate
The event is a standard `generate` event: the picked element's context, a preflighted scaffold, and `_instructions` naming the action's reference, the planning section, and the exact splice. Handle it exactly per live.md's **Handle generate**, which owns everything from the identity lock to the done reply: read the action's reference and craft-floor.md as it says, plan per section 4 (identity first, then mode, then three different primary axes, then the squint test), declare knobs per section 7, and deliver per section 6 (a complete replacement of the element per variant, the preview CSS plus every variant in one edit at the scaffold's splice). The lane changes nothing about what a variant may be: the moves a live session would make on this element (a promoted tier, a restructured set, a reordered card, a different surface) are open here too. Never screenshot the page; the overlay preview is the review channel until accept.
**Reply and wait in one call**, with the file you wrote:
```bash
.agent/skills/impeccable/scripts/impeccable live-poll --reply EVENT_ID done --file src/App.jsx --then-poll
```
This replies done (the browser mounts the variants) and then blocks until the user's choice arrives, so run it the way your harness runs a long wait: **Claude Code** in the foreground with your tool's longest timeout (600000 ms), so you are paused until the choice arrives; **Codex** in a yielded foreground exec; **Cursor** in a background terminal with notify on `"type":"(accept|discard|variant_mount_failed|exit)"`. Never pass a short `--timeout=`. While it runs there is nothing else to do: never sleep and never poll its output on a timer; a harness that backgrounds it wakes you when it returns. `{"type":"timeout"}` means the user has not chosen yet: run `live-poll` again and keep waiting. If the edit fails after the browser flipped to GENERATING, `--reply EVENT_ID error "Short reason"` (without `--then-poll`) so the bar resets.
Then tell the user, in one line, where their variants are: *"Three [bolder] variants are live on [the pricing cards]: cycle with the floating bar's arrows, adjust the Tune knobs, and Accept the keeper."*
Outside the replace path, read the matching live.md section before acting: `scaffold.previewMode: "svelte-component"` (Svelte previews are edited as components, and their accept is mechanical), `mode: "insert"`, `variant_mount_failed`, `steer`, `manual_edit_apply`, and any `fallback: "agent-driven"` wrap error.
## Step 4: Accept and close
The call from Step 3 returns the user's choice. **`discard`**: nothing to do. **`accept`**: `_acceptResult.carbonize: true` is the normal case, and the cleanup is live.md's **Required after accept**, unchanged: move the accepted variant's rules into the stylesheet that already owns the element with real selectors, bake the chosen knob values in, unwrap the element and drop every `data-impeccable-*` attribute, delete the inline `<style>` block and both `impeccable-carbonize` markers, then `.agent/skills/impeccable/scripts/impeccable live-complete --id SESSION_ID` and confirm `phase: "completed"`. (`baked: true` appears only when the accept was run with `--bake`; then the helper already made the variant permanent and no `live-complete` is owed.)
Close without being asked, the moment the choice is handled:
```bash
.agent/skills/impeccable/scripts/impeccable live-server stop
```
Stopping removes the injected script and reloads the page once: the user sees the accepted design with no overlay chrome, still served by their dev server. **Never kill or restart the dev server**, including one you started in Step 2.
- **The user asks for more variants before you closed**: skip the close, run Step 2 again for the next element (the helper is reused), and close after the last choice.
- **Interrupted or unsure of the state**: `.agent/skills/impeccable/scripts/impeccable live-status`, then `live-resume`; the journal under `.impeccable/live/sessions/` is canonical.
Done when the helper is stopped and the dev site still answers with the accepted design.
@@ -205,6 +205,11 @@ t('items', { count }) // Handles complex plural rules
- Optimistic updates with rollback - Optimistic updates with rollback
- Conflict resolution - Conflict resolution
**Interrupted gestures** (custom sliders, drag surfaces, scrollable control strips):
- A second finger or pointer lands mid-drag: the first drag keeps its pointer or ends cleanly, never jumps to the new one
- The browser cancels the gesture to scroll (`pointercancel`), capture is lost (`lostpointercapture`), the pointer is released outside the control, or the window loses focus (`blur`) mid-drag: clear the dragging state and release capture
- After each of these, the next tap or drag works without a reload
**Permission states**: **Permission states**:
- No permission to view - No permission to view
- No permission to edit - No permission to edit
@@ -304,6 +309,7 @@ const throttledScroll = throttle(handleScroll, 100);
- Unit tests for edge cases - Unit tests for edge cases
- Integration tests for error scenarios - Integration tests for error scenarios
- E2E tests for critical paths - E2E tests for critical paths
- A behavioral regression for each confirmed gesture fix, when the project's test runner can drive input
- Visual regression tests - Visual regression tests
- Accessibility tests (axe, WAVE) - Accessibility tests (axe, WAVE)
@@ -330,7 +336,10 @@ Test thoroughly with edge cases:
- **Network issues**: Disable internet, throttle connection - **Network issues**: Disable internet, throttle connection
- **Large datasets**: Test with 1000+ items - **Large datasets**: Test with 1000+ items
- **Concurrent actions**: Click submit 10 times rapidly - **Concurrent actions**: Click submit 10 times rapidly
- **Interrupted gestures**: Add a second finger mid-drag, scroll across the control, release outside it, switch windows mid-drag; then drag again
- **Errors**: Force API errors, test all error states - **Errors**: Force API errors, test all error states
- **Empty**: Remove all data, test empty states - **Empty**: Remove all data, test empty states
For gestures, say what produced the evidence (emulated viewport, synthesized touch, which engine, physical device) and name what stayed untested.
When edge cases are covered, hand off to `/impeccable polish` for the final pass. When edge cases are covered, hand off to `/impeccable polish` for the final pass.
@@ -96,7 +96,7 @@ Build the assigned direction, not a safer interpretation of it. The form supplie
When an approved comp exists, it is a spatial contract, not a mood board: only the user can downgrade its authority, in explicit words. Models systematically believe their HTML, CSS, and SVG recreation of an image succeeded when it did not, so the build runs as a state machine on disk whose gates measure the screen against the comp instead of asking you to remember it. Start it once, and let it tell you what is next: When an approved comp exists, it is a spatial contract, not a mood board: only the user can downgrade its authority, in explicit words. Models systematically believe their HTML, CSS, and SVG recreation of an image succeeded when it did not, so the build runs as a state machine on disk whose gates measure the screen against the comp instead of asking you to remember it. Start it once, and let it tell you what is next:
`.agent/skills/impeccable/scripts/impeccable build-phase start --direction <seed key> --kind <assigned|pick|challenger|canon> --artifact <entry file>` right after the direction choice (this is also the choice ping; the roll's output names the exact command), or `start --comp <approved comp> --artifact <entry file>` when a surface round already locked one. `.agent/skills/impeccable/scripts/impeccable build-phase start --direction <seed key> --kind <assigned|pick|challenger|canon>` right after the direction choice (this is also the choice ping; the roll's output names the exact command), or `start --comp <approved comp>` when a surface round already locked one.
Then, in order, each closed by `.agent/skills/impeccable/scripts/impeccable build-phase advance` (every verb below runs as `.agent/skills/impeccable/scripts/impeccable <verb>`; exit 2 means the gate failed and printed why; fix that and advance again; write nothing for a later phase while an earlier gate is open): Then, in order, each closed by `.agent/skills/impeccable/scripts/impeccable build-phase advance` (every verb below runs as `.agent/skills/impeccable/scripts/impeccable <verb>`; exit 2 means the gate failed and printed why; fix that and advance again; write nothing for a later phase while an earlier gate is open):
@@ -104,9 +104,8 @@ Then, in order, each closed by `.agent/skills/impeccable/scripts/impeccable buil
The comp-led path is a frontier-tier job: it asks the builder to hold a measured layout, place plates at their boxes, and act on numeric readings across a dozen attempts. Smaller or faster models produce a recognisable page and stall under the hero gate; if the model in hand is one of those, say so before the direction round and take the code-led path, or expect the run to end at the hero with its readings unmet. The comp-led path is a frontier-tier job: it asks the builder to hold a measured layout, place plates at their boxes, and act on numeric readings across a dozen attempts. Smaller or faster models produce a recognisable page and stall under the hero gate; if the model in hand is one of those, say so before the direction round and take the code-led path, or expect the run to end at the hero with its readings unmet.
1. **spec.** Measure the comp: `impeccable comp-spec --comp <comp> --grid` writes a coordinate grid over the comp; open it, name every salient region by grid span in a regions file (text and control regions snap to the largest ink mass inside their span, so a headline named B1:E4 measures as the headline and not the column beside it; `snap: false` keeps the span, and an explicit `box` is taken as drawn) (kind `plate` / `image` / `texture` for anything painted: every illustration, photograph, figure, product object, and material texture; `text` / `control` / `chrome` for what code draws; every region carries a `note` saying what the comp shows there, which the plate prompt and the gate messages read), and run `impeccable comp-spec --comp <comp> --regions <file>`. The spec carries each region's box, sampled palette, and medium; `impeccable comp-spec --print` is the build's reference from here on. Type is measured, not guessed: `impeccable font-match --measure <text region>` reads the comp's cap height, width class, and weight off the pixels, and `impeccable font-match --rank <region> --text "..."` takes its candidates from a fingerprint index of the Google Fonts catalog (the nearest faces to the crop's shape) plus any names you pass with `--candidates`, renders them at that cap height with the region's words, and ranks them by fingerprint distance (its `USE` line is the CSS; its proof sheet shows the comp over the top three); with no browser resolvable it records the catalog's nearest face and says the size is estimated, which is still the choice to build on. Do not install a browser to rank, and never write a `chosen` face into the spec by hand: the gate accepts only what font-match wrote. The spec gate refuses to close until the lead text region is measured and ranked. A region note that describes painted material (a diagram, drawing, photograph, texture) under a code kind is refused at the spec: reclassify it as a plate, or reword the note if code really draws it. The script refuses a regions file that leaves comp ink unnamed (callouts, a parts table, a notes block): what is never named can never be missing, so everything the comp shows gets a region. It also refuses a `text` / `control` / `chrome` region larger than a quarter of the comp: that is a column, not an element, and a column scored as one region hides the plates, tables, and notes inside it. Name each element inside it (`container: true` only when it truly is one undivided element). Anything drawn is a plate: an inline SVG past an icon's budget (a diagram, notation, leader lines with arrows, a "quick approximation" of the artwork) is refused at the hero; icon-sized SVG (under 64px, a few paths) is fine, and a chart the page draws from data at runtime is a chart, not an illustration. Callout lines and arrows that annotate a drawing belong to that drawing's plate, with only their labels set as text. A crop of the comp is never a plate (the plates gate refuses a file that is a resample of the comp region: the comp's grain, its neighbours' edges, and its resolution would ship as the artwork); the crop is the reference the plate is generated from. A plate region's box has to hold its whole artwork with a margin: the spec measures the artwork's contact with the box edges and refuses a box that cuts through it (`bleed: true` only when the page really crops it there), because a plate placed with `object-fit: cover` on such a box shows the artwork minus the side the box lost. Anything not in the spec does not exist on the page: no borders, rules, containers, or chrome the comp does not show. Only three concessions exist: fonts (the closest obtainable face), icon glyphs (close enough, exact if the user chose an icon library; this covers the pictogram only, never a control's chrome, so a chevron, an arrow, a dropdown's border and fill, a button's shape are the comp's), and genuine defects in the comp such as spelling errors. 1. **spec.** Measure the comp: `impeccable comp-spec --comp <comp> --grid` writes a coordinate grid over the comp; open it, name every salient region by grid span in a regions file (text and control regions snap to the largest ink mass inside their span, so a headline named B1:E4 measures as the headline and not the column beside it; `snap: false` keeps the span, and an explicit `box` is taken as drawn) (kind `plate` / `image` / `texture` for anything painted: every illustration, photograph, figure, product object, and material texture; `text` / `control` / `chrome` for what code draws; every region carries a `note` saying what the comp shows there, which the plate prompt and the gate messages read), and run `impeccable comp-spec --comp <comp> --regions <file>`. The spec carries each region's box, sampled palette, and medium; `impeccable comp-spec --print` is the build's reference from here on. Type is measured, not guessed: `impeccable font-match --measure <text region>` reads the comp's cap height, width class, and weight off the pixels, and `impeccable font-match --rank <region> --text "..."` takes its candidates from a fingerprint index of the Google Fonts catalog (the nearest faces to the crop's shape) plus any names you pass with `--candidates`, renders them at that cap height with the region's words, and ranks them by fingerprint distance (its `USE` line is the CSS; its proof sheet shows the comp over the top three); with no browser resolvable it records the catalog's nearest face and says the size is estimated, which is still the choice to build on. Do not install a browser to rank, and never write a `chosen` face into the spec by hand: the gate accepts only what font-match wrote. The spec gate refuses to close until the lead text region is measured and ranked. A region note that describes painted material (a diagram, drawing, photograph, texture) under a code kind is refused at the spec: reclassify it as a plate, or reword the note if code really draws it. The script refuses a regions file that leaves comp ink unnamed (callouts, a parts table, a notes block): what is never named can never be missing, so everything the comp shows gets a region. It also refuses a `text` / `control` / `chrome` region larger than a quarter of the comp: that is a column, not an element, and a column scored as one region hides the plates, tables, and notes inside it. Name each element inside it (`container: true` only when it truly is one undivided element). Anything drawn is a plate: an inline SVG past an icon's budget (a diagram, notation, leader lines with arrows, a "quick approximation" of the artwork) is refused at the hero; icon-sized SVG (under 64px, a few paths) is fine, and a chart the page draws from data at runtime is a chart, not an illustration. Callout lines and arrows that annotate a drawing belong to that drawing's plate, with only their labels set as text. A crop of the comp is never a plate (the plates gate refuses a file that is a resample of the comp region: the comp's grain, its neighbours' edges, and its resolution would ship as the artwork); the crop is the reference the plate is generated from. A plate region's box has to hold its whole artwork with a margin: the spec measures the artwork's contact with the box edges and refuses a box that cuts through it (`bleed: true` only when the page really crops it there), because a plate placed with `object-fit: cover` on such a box shows the artwork minus the side the box lost. Anything not in the spec does not exist on the page: no borders, rules, containers, or chrome the comp does not show. Only three concessions exist: fonts (the closest obtainable face), icon glyphs (close enough, exact if the user chose an icon library; this covers the pictogram only, never a control's chrome, so a chevron, an arrow, a dropdown's border and fill, a button's shape are the comp's), and genuine defects in the comp such as spelling errors.
2. **plates.** Every raster region ships as a plate: an illustration, photo, or figure regenerated at asset resolution from its comp crop, UI text removed, at its `plate` path (isolated ink, figures, or objects use native transparent PNG so they sit on the page's own ground; photos and textures stay opaque); a texture (paper, cloth, grain) is a clean patch of the comp region mirror-tiled to size, generated only when no clean patch exists. `impeccable comp-spec --crop <id>` writes the reference; save `impeccable comp-spec --plate-prompt <id> --background transparent` to a prompt file for a cutout, or use `--background opaque` otherwise. Prefer the harness-native image tool with that crop and prompt, then `impeccable embed-prompt <plate> --prompt-file <prompt.txt>`. The API fallback is `impeccable generate-image --ref <crop.png> --prompt-file <prompt.txt> --out <plate.png> --size <WxH> --quality high --background transparent` (use `--background opaque` for full-frame assets); create the output directory first. Verify actual alpha, white foregrounds, fine edges, and clear holes on light and dark grounds; do not chroma-key native output. After the initial assets exist, prepare the isolated code component previews and complete [component-review.md](component-review.md) before further gate-driven repair: the user reviews the whole component kit, including code, before page assembly. This checkpoint keeps the existing gates; it does not require passing them first. After the full page and responsive checks, use the assembled-hero checkpoint before the final response. The plates gate scores the assets against the comp; also inspect placement and scale visually. With parallel subagents, spawn the shipped asset producer (`impeccable-asset-producer`; `impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent") with the spec path and let it produce them all; without subagents, produce them here. A crop of the comp is a reference, never a shipping pixel. The gate checks every plate exists, is at least 1.5x the region's size, and reads as the region. Page code waits for this gate: a page written before its plates exist is a page that draws its material in CSS. A single-file deliverable changes nothing here: the plate is produced the same way and inlined as a data URI. `--force` exists for one case only, the user downgrading the comp's authority in words you quote in `--reason`; the script refuses every other reason. 2. **plates.** Every raster region ships as a plate: an illustration, photo, or figure regenerated at asset resolution from its comp crop, UI text removed, at its `plate` path (isolated ink, figures, or objects use native transparent PNG so they sit on the page's own ground; photos and textures stay opaque); a texture (paper, cloth, grain) is a clean patch of the comp region mirror-tiled to size, generated only when no clean patch exists. `impeccable comp-spec --crop <id>` writes the reference; save `impeccable comp-spec --plate-prompt <id> --background transparent` to a prompt file for a cutout, or use `--background opaque` otherwise. Prefer the harness-native image tool with that crop and prompt, then `impeccable embed-prompt <plate> --prompt-file <prompt.txt>`. The API fallback is `impeccable generate-image --ref <crop.png> --prompt-file <prompt.txt> --out <plate.png> --size <WxH> --quality high --background transparent` (use `--background opaque` for full-frame assets); create the output directory first. Verify actual alpha, white foregrounds, fine edges, and clear holes on light and dark grounds; do not chroma-key native output. The plates gate scores the assets against the comp; also inspect placement and scale visually. With parallel subagents, spawn the shipped asset producer (`impeccable-asset-producer`; `impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent") with the spec path and let it produce them all; without subagents, produce them here. A crop of the comp is a reference, never a shipping pixel. The gate checks every plate exists, is at least 1.5x the region's size, and reads as the region. Page code waits for this gate: a page written before its plates exist is a page that draws its material in CSS. A single-file deliverable changes nothing here: the plate is produced the same way and inlined as a data URI. `--force` exists for one case only, the user downgrading the comp's authority in words you quote in `--reason`; the script refuses every other reason.
3. **hero.** `impeccable build-phase scaffold` first: it writes the measured layout as CSS custom properties (`.impeccable/build/scaffold/layout.css`: `--r-<id>-x/y/w/h` in % of the comp, plus cap height, font-size, family, and weight where measured) and a reference page (`hero-reference.html`) with every region at its box and every plate placed. Bind the numbers to your own semantic structure, an element per region; the reference is a check on positions, never the page, and overlapping boxes are overlapping boxes. Then build only the first viewport, at the comp's own dimensions, the comp's words copied verbatim (the user approved that comp with those words; rewording is a stated decision after the hero passes, never a silent one inside it), every text region sized from its measured cap height and set in its ranked face, plates first: place every plate at its spec box (`object-fit: cover`, an `<img>`, a background image, or an inlined data URI named for it) before any text or control, capture into `.impeccable/review/hero-repro.png`, run `impeccable build-phase record hero` once so you see the plate regions read as match before any text exists, then lay the semantic layer over the plates from the spec's palette and boxes and advance. The gate first refuses while any plate is unreferenced by the source, then runs `impeccable comp-diff`, writes `.impeccable/review/diff/hero/` (side-by-side, heatmap, one paired crop per region, `report.json`), and passes at 72% overall with no hard veto outstanding (a missing region, a contradicted plate or text block, an SVG illustration, a clipped plate, invented ink block at any score); above the bar, the numeric readings become advisories printed with the pass, and the polish pass before responsive is where they get fixed: the gate also reads each text region's cap height, line count, weight, ink colour, and position against the comp, each chrome strip's height off its rule, and the frame for ink where the comp is calm (a kicker, an extra nav item, a divider), and says each miss as a number ("cap height 78px in the build, 103px in the comp"); those numbers are the edit. When it fails, open the region crops it lists, in order, before editing: a region scored `missing` needs its material, `contradicted` needs its structure re-derived from the spec box, `drift` is where size and spacing edits belong; the gate refuses a third attempt that only nudges values on the same region. This is where the run's ambition is won or lost, and a retry here costs minutes where a rebuild verdict at the finish costs the run.
3. **hero.** `impeccable build-phase scaffold` first: it writes the measured layout as CSS custom properties (`.impeccable/build/scaffold/layout.css`: `--r-<id>-x/y/w/h` in % of the comp, plus cap height, font-size, family, and weight where measured) and a reference page (`hero-reference.html`) with every region at its box and every plate placed. Bind the numbers to your own semantic structure, an element per region; the reference is a check on positions, never the page, and overlapping boxes are overlapping boxes. Then build only the first viewport, at the comp's own dimensions, the comp's words copied verbatim (the user approved that comp with those words; rewording is a stated decision after the hero passes, never a silent one inside it), every text region sized from its measured cap height and set in its ranked face, plates first: place every plate at its spec box (`object-fit: cover`, an `<img>`, a background image, or an inlined data URI named for it) before any text or control, capture into `.impeccable/review/hero-repro.png`, run `impeccable build-phase record hero` once so you see the plate regions read as match before any text exists, then lay the semantic layer over the plates from the spec's palette and boxes and advance. The gate first refuses while any plate is unreferenced by the source, then runs `impeccable comp-diff`, writes `.impeccable/review/diff/hero/` (side-by-side, heatmap, one paired crop per region, `report.json`; `raw-report.json` preserves the uninterpreted measurements). The report and crop labels use the gate's verdicts; `gate.reasons` lists the remaining blockers even when a region is called drift. An accepted plate is revalidated if its file, measured region, or comp changes. The gate passes at 72% overall with no hard veto outstanding (a missing region, a contradicted plate or text block, an SVG illustration, a clipped plate, invented ink block at any score); above the bar, the numeric readings become advisories printed with the pass, and the polish pass before responsive is where they get fixed: the gate also reads each text region's cap height, line count, weight, ink colour, and position against the comp, each chrome strip's height off its rule, and the frame for ink where the comp is calm (a kicker, an extra nav item, a divider), and says each miss as a number ("cap height 78px in the build, 103px in the comp"); those numbers are the edit. When it fails, open the region crops it lists, in order, before editing: a region scored `missing` needs its material, `contradicted` needs its structure re-derived from the spec box, `drift` is where size and spacing edits belong; repeated attempts do not clear unresolved blockers. This is where the run's ambition is won or lost, and a retry here costs minutes where a rebuild verdict at the finish costs the run.
4. **sections.** Build the rest of the surface inside the spec's system: the same corner language, line weights, and palette, and nothing the comp never shows. Where the comp does not cover a region, it inherits the recorded system. 4. **sections.** Build the rest of the surface inside the spec's system: the same corner language, line weights, and palette, and nothing the comp never shows. Where the comp does not cover a region, it inherits the recorded system.
5. **motion.** The signature interaction, reveals, and motion, orchestrated once rather than scattered. 5. **motion.** The signature interaction, reveals, and motion, orchestrated once rather than scattered.
6. **responsive.** The other viewports, and the first viewport at common desktop widths (1280 to 1600), not only at the comp's exact size: fluid columns, no fixed-pixel grid that wraps a hundred pixels narrower. Capture `desktop.png` (1440 wide, full page) and `mobile.png` (390 wide) into `.impeccable/review/`; the gate diffs the desktop capture against the comp and refuses a first viewport that only held at the comp's width. A comp'd surface that is mobile-first was comped portrait; the plates were produced for that frame. 6. **responsive.** The other viewports, and the first viewport at common desktop widths (1280 to 1600), not only at the comp's exact size: fluid columns, no fixed-pixel grid that wraps a hundred pixels narrower. Capture `desktop.png` (1440 wide, full page) and `mobile.png` (390 wide) into `.impeccable/review/`; the gate diffs the desktop capture against the comp and refuses a first viewport that only held at the comp's width. A comp'd surface that is mobile-first was comped portrait; the plates were produced for that frame.
@@ -144,5 +143,3 @@ A rebuild and a fix round share one asset rule: a raster either round creates or
Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice.
After the last correction, spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, artifact path, direction contract, PRODUCT.md, [document.md](document.md), and write boundary. Without subagents, load [degraded/documenter.md](degraded/documenter.md) and [document.md](document.md) before writing. Verify the outcome: new worlds and approved system changes require token-bearing DESIGN.md **and** `.impeccable/design.json`, not prose alone. Ordinary extensions compare the finished build against the incumbent system, preserve its files, and report the evidence checked; report pre-existing drift without repairing it unasked. Recheck after later edits. Finish only when review and documentation are complete. After the last correction, spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, artifact path, direction contract, PRODUCT.md, [document.md](document.md), and write boundary. Without subagents, load [degraded/documenter.md](degraded/documenter.md) and [document.md](document.md) before writing. Verify the outcome: new worlds and approved system changes require token-bearing DESIGN.md **and** `.impeccable/design.json`, not prose alone. Ordinary extensions compare the finished build against the incumbent system, preserve its files, and report the evidence checked; report pre-existing drift without repairing it unasked. Recheck after later edits. Finish only when review and documentation are complete.
On a comp-led build, record the final review disposition with `.agent/skills/impeccable/scripts/impeccable build-phase finish --disposition <ship|fix|rebuild|recapture>` before the final response. A refused `ship` is an unfinished build; report the outstanding phase with the verdict.
@@ -16,7 +16,7 @@ Reason over the signals; there is no score to obey:
- `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique <surface>` is a strong default. - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique <surface>` is a strong default.
- `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog and closes it when stale or cleared). - `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog and closes it when stale or cleared).
- `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them.
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. **`live` and the bundled `impeccable detect` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with either; the browser overlay and the HTML rule engine don't apply to native app code. - `devServer.running` true → `live` is available for in-browser iteration, and `generate` for one-shot variant runs on a named element; if false, don't lead with either. **`live`, `generate`, and the bundled `impeccable detect` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with any of them; the browser overlay and the HTML rule engine don't apply to native app code.
- Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`. - Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`.
**If `scan.targets` is non-empty and `setup.platform` is not `ios`/`android`/`adaptive`, run `.agent/skills/impeccable/scripts/impeccable detect --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx; it reads HTML/CSS, so skip it for native projects). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it. **If `scan.targets` is non-empty and `setup.platform` is not `ios`/`android`/`adaptive`, run `.agent/skills/impeccable/scripts/impeccable detect --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx; it reads HTML/CSS, so skip it for native projects). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
+1 -1
View File
@@ -1 +1 @@
0.1.6 0.1.5
@@ -19,6 +19,10 @@
"description": "Interactive live variant mode. Select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via HMR. Requires a running dev server. Use when you want to visually experiment with design alternatives in real time.", "description": "Interactive live variant mode. Select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via HMR. Requires a running dev server. Use when you want to visually experiment with design alternatives in real time.",
"argumentHint": "" "argumentHint": ""
}, },
"generate": {
"description": "Agent-driven live variant generation. Boots live mode, finds the named element on the open page, scrolls the browser to it, and delivers N variants in the requested direction for the user to cycle and accept. Use for requests that name an element and a direction, like 'generate 3 bold variants of the pricing cards', skipping manual element picking.",
"argumentHint": "[count] [direction] variants of [element]"
},
"adapt": { "adapt": {
"description": "Adapt designs to work across different screen sizes, devices, contexts, or platforms. Implements breakpoints, fluid layouts, and touch targets. Use when the user mentions responsive design, mobile layouts, breakpoints, viewport adaptation, or cross-device compatibility.", "description": "Adapt designs to work across different screen sizes, devices, contexts, or platforms. Implements breakpoints, fluid layouts, and touch targets. Use when the user mentions responsive design, mobile layouts, breakpoints, viewport adaptation, or cross-device compatibility.",
"argumentHint": "[target] [context (mobile, tablet, print...)]" "argumentHint": "[target] [context (mobile, tablet, print...)]"
@@ -165,6 +165,14 @@
} }
let parameterGenerationState = 'idle'; let parameterGenerationState = 'idle';
let parameterReadyAnnouncedSession = null; let parameterReadyAnnouncedSession = null;
// 'agent' when the generate verb fired this session's Go (the generate
// lane declares no knobs, so its bar never shows a pending Tune chip);
// null for every Go a user presses.
let sessionOrigin = null;
// The generate lane picks for the agent and never edits copy in the
// browser, so its selection carries no edit-copy badge (set on the
// agent-target pick, cleared with the session; a user's pick never sets it).
let editBadgeSuppressed = false;
let svelteComponentSession = null; let svelteComponentSession = null;
let svelteRuntimePromise = null; let svelteRuntimePromise = null;
let pendingSvelteComponentRetryObserver = null; let pendingSvelteComponentRetryObserver = null;
@@ -983,9 +991,20 @@
} }
} catch { /* cross-origin */ } } catch { /* cross-origin */ }
} }
// The selector a mechanical bake would anchor lasting rules on, and how
// many elements it matches right now: the bake refuses anything but one,
// since its rules would restyle every match, not just this element.
const cssIdent = (s) => /^[A-Za-z_-][\w-]*$/.test(s);
const anchorClasses = [...el.classList].filter(cssIdent);
const anchor = el.id && cssIdent(el.id)
? '#' + el.id
: (anchorClasses.length ? el.tagName.toLowerCase() + '.' + anchorClasses.join('.') : null);
let anchorMatches = null;
if (anchor) { try { anchorMatches = document.querySelectorAll(anchor).length; } catch { anchorMatches = null; } }
return { return {
tagName: el.tagName.toLowerCase(), id: el.id || null, tagName: el.tagName.toLowerCase(), id: el.id || null,
classes: [...el.classList], classes: [...el.classList],
anchor, anchorMatches,
textContent: (el.textContent || '').slice(0, 500), textContent: (el.textContent || '').slice(0, 500),
outerHTML: sanitizedContextOuterHTML(el, 10000), outerHTML: sanitizedContextOuterHTML(el, 10000),
computedStyles: { computedStyles: {
@@ -2037,6 +2056,7 @@
function setLiveState(next) { function setLiveState(next) {
state = next; state = next;
window.__IMPECCABLE_LIVE_STATE__ = next; window.__IMPECCABLE_LIVE_STATE__ = next;
retryDeclinedAgentTargets();
syncPageInteractionCursor(); syncPageInteractionCursor();
// Whether a queued steer is still behind a generation is a function of this // Whether a queued steer is still behind a generation is a function of this
// state, so the hint has to move with it, not only with the 5s poll. // state, so the hint has to move with it, not only with the 5s poll.
@@ -4014,6 +4034,7 @@
function hidePendingApplyDock() { function hidePendingApplyDock() {
pendingApplyInFlight = false; pendingApplyInFlight = false;
retryDeclinedAgentTargets();
clearStoredManualApplyState(); clearStoredManualApplyState();
if (pendingIntroAnimation) { pendingIntroAnimation.cancel(); pendingIntroAnimation = null; } if (pendingIntroAnimation) { pendingIntroAnimation.cancel(); pendingIntroAnimation = null; }
if (pendingDockEl) pendingDockEl.style.display = 'none'; if (pendingDockEl) pendingDockEl.style.display = 'none';
@@ -4047,6 +4068,7 @@
function setPendingApplyLoading(loading, count) { function setPendingApplyLoading(loading, count) {
if (!pendingPillEl || !pendingPillLabelEl || !pendingPillCountEl || !pendingTrashBtn) return; if (!pendingPillEl || !pendingPillLabelEl || !pendingPillCountEl || !pendingTrashBtn) return;
pendingApplyInFlight = loading === true; pendingApplyInFlight = loading === true;
if (!pendingApplyInFlight) retryDeclinedAgentTargets();
const currentCount = count || parseInt(pendingPillEl.dataset.count || '0', 10) || 0; const currentCount = count || parseInt(pendingPillEl.dataset.count || '0', 10) || 0;
if (pendingApplyInFlight) storeManualApplyState(currentCount); if (pendingApplyInFlight) storeManualApplyState(currentCount);
else clearStoredManualApplyState(); else clearStoredManualApplyState();
@@ -4688,6 +4710,7 @@
} }
function renderEditBadge(mode) { function renderEditBadge(mode) {
if (editBadgeSuppressed || sessionOrigin === 'agent') mode = 'hidden';
if (mode === 'hidden' || !editBadgeEl) { if (mode === 'hidden' || !editBadgeEl) {
hideConfigureBarTooltip(); hideConfigureBarTooltip();
if (editBadgeEl) editBadgeEl.style.display = 'none'; if (editBadgeEl) editBadgeEl.style.display = 'none';
@@ -6181,6 +6204,8 @@
resetSessionFileMeta(); resetSessionFileMeta();
currentSessionId = null; currentSessionId = null;
parameterGenerationState = 'idle'; parameterGenerationState = 'idle';
sessionOrigin = null;
editBadgeSuppressed = false;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
expectedVariants = 0; expectedVariants = 0;
arrivedVariants = 0; arrivedVariants = 0;
@@ -7112,6 +7137,398 @@
} }
// //
// ------------------------------------------------------------------
// Agent-initiated targeting (the `generate` command). The agent names an
// element by CSS selector over POST /agent-target; the server pushes an
// `agent_target` SSE message here. The overlay resolves the selector,
// scrolls the element into view, enters the same picked state a user
// click produces, and fires the normal Go pipeline, so everything
// downstream (generate event, variants, cycling, accept) is unchanged.
// The verdict goes back through POST /agent-target-result, which resolves
// the agent's held-open CLI call.
function postAgentTargetResult(targetId, result) {
fetch('http://localhost:' + PORT + '/agent-target-result?token=' + TOKEN, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: TOKEN, targetId, clientId: AGENT_TARGET_CLIENT_ID, ...result }),
}).catch(() => { /* server gone; nothing to report to */ });
}
function describeAgentTargetCandidate(el) {
return {
tag: el.tagName.toLowerCase(),
id: el.id || null,
classes: [...el.classList].filter((c) => !c.startsWith('impeccable-')),
text: (el.textContent || '').trim().slice(0, 80),
};
}
function resolveAgentTargetElement(msg) {
let matched;
try {
matched = [...document.querySelectorAll(msg.selector)];
} catch {
return { error: { ok: false, error: 'invalid_selector', selector: msg.selector } };
}
let candidates = matched.filter((el) => pickable(el));
if (msg.text) {
const needle = String(msg.text).toLowerCase();
candidates = candidates.filter((el) => (el.textContent || '').toLowerCase().includes(needle));
}
if (candidates.length === 0) {
return {
error: {
ok: false,
error: 'no_match',
selector: msg.selector,
matchCount: 0,
// How many nodes the raw selector hit before the pickable/text
// filters: distinguishes a wrong selector from an unpickable match.
rawMatchCount: matched.length,
},
};
}
if (Number.isInteger(msg.index)) {
const el = candidates[msg.index - 1];
if (!el) {
return { error: { ok: false, error: 'index_out_of_range', selector: msg.selector, matchCount: candidates.length } };
}
return { el, matchCount: candidates.length };
}
if (candidates.length > 1) {
return {
error: {
ok: false,
error: 'ambiguous',
selector: msg.selector,
matchCount: candidates.length,
candidates: candidates.slice(0, 8).map(describeAgentTargetCandidate),
},
};
}
return { el: candidates[0], matchCount: 1 };
}
function scrollAgentTargetIntoView(el, done) {
const rect = el.getBoundingClientRect();
if (rect.top >= 0 && rect.bottom <= window.innerHeight) { done(); return; }
let settled = false;
let fallback = null;
const finish = () => {
if (settled) return;
settled = true;
removeEventListener('scrollend', finish, true);
if (fallback) clearTimeout(fallback);
done();
};
// scrollend where supported; a timer covers engines without it and the
// no-movement case (element already at its final resting position).
addEventListener('scrollend', finish, true);
fallback = setTimeout(finish, 1200);
el.scrollIntoView({ block: 'center', behavior: 'smooth' });
}
// One id per page load: the server keys claims and roll-call reports on
// it, and only the tab that holds the lease can renew it.
const AGENT_TARGET_CLIENT_ID = id8();
// The agent target an agent-initiated Go is serving: set by
// actOnAgentTarget around its handleGo call, read once by handleGo.
let agentTargetForGo = null;
// The helper's word on its global bar. The generate lane asks the helper
// to keep it out of the way (`impeccable live --no-live-bar`, or an agent
// target carrying hideLiveBar), and the helper tells every connected tab
// at once (`live_bar`) and every later connection on `connected`, so the
// bar stays hidden in every tab, through reloads, the accept, and the
// bake, until the helper stops and takes the overlay with it. The variant
// controls still show.
let liveBarHiddenByHelper = false;
function applyLiveBarPreference(hidden) {
liveBarHiddenByHelper = hidden === true;
setLiveBarHidden(liveBarHiddenByHelper);
}
// A plain live session must never notice this code: hiding remembers the
// bar's own display value and restoring puts exactly that back, and a
// restore on a bar that is not hidden is a no-op, so the `connected`
// frame every session receives changes nothing unless the lane asked.
function setLiveBarHidden(hidden) {
if (!globalBarEl) return;
if (hidden) {
if (globalBarEl.style.display !== 'none') {
globalBarEl.dataset.liveBarDisplay = globalBarEl.style.display || 'flex';
globalBarEl.style.display = 'none';
}
return;
}
if (globalBarEl.style.display === 'none') {
globalBarEl.style.display = globalBarEl.dataset.liveBarDisplay || 'flex';
}
}
function claimAgentTarget(targetId, report) {
return fetch('http://localhost:' + PORT + '/agent-target-claim?token=' + TOKEN, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: TOKEN, targetId, clientId: AGENT_TARGET_CLIENT_ID, ...report }),
}).then((res) => res.json())
.then((j) => ({ granted: !!j && j.granted === true, pending: !!j && j.pending === true }))
.catch(() => ({ granted: false, pending: false }));
}
// `exceptTargetId` is the target this call is about: a tab acting on it
// is not busy for itself, but it is busy for every other target, or two
// held requests could both be claimed here and the second Go would
// overwrite the session the first one minted.
function agentTargetBusyReason(exceptTargetId) {
if (pendingApplyInFlight) return 'manual_apply_in_flight';
if (state !== 'IDLE' && state !== 'PICKING' && state !== 'CONFIGURING') return 'session_active';
for (const [targetId, status] of agentTargetsSeen) {
if (status === 'acting' && targetId !== exceptTargetId) return 'agent_target_in_flight';
}
return null;
}
// Targets this tab declined as busy. A busy report is only this tab's word
// at that moment: the moment it is free again (setLiveState), it claims
// each of these as eligible, and the server drops the stale report, so a
// busy verdict is never built on a tab that has since gone idle. The
// server denies claims for resolved targets, so retries are harmless.
const busyDeclinedTargets = new Map();
function declineAgentTargetBusy(msg, busy) {
busyDeclinedTargets.set(msg.targetId, msg);
noteAgentTarget(msg.targetId, 'declined');
claimAgentTarget(msg.targetId, { eligible: false, state, reason: busy });
}
// A torn-down overlay, or one whose helper connection is gone, cannot
// serve a target and must not even claim one: it would hold the lease for
// a request it will never act on.
function agentTargetOverlayGone() {
return !evtSource;
}
// A denied claimant retries at this cadence, a little over the lease, so
// the first retry after a dead holder's lease lapses is granted.
const AGENT_TARGET_RESCUE_RETRY_MS = 3500;
// Claim the lease and act as the holder. A denied claim means another tab
// holds the lease. That holder can die before posting its result (reload,
// crash, even after renewing), and its lease lapses after ~3s, so this tab
// keeps retrying for as long as the server still holds the request: the
// answer's `pending` is the server's word that the request is alive, and
// it turns false the moment the request resolved or timed out, so no tab
// retries a request nobody awaits. A tab that turned busy meanwhile joins
// the roll call instead of taking a lease it cannot use. The first claim
// and the busy-to-idle re-claim share this.
function claimAndActOnAgentTarget(msg) {
if (agentTargetOverlayGone()) return;
const busy = agentTargetBusyReason(msg.targetId);
if (busy) { declineAgentTargetBusy(msg, busy); return; }
if (declineAgentTargetUnresolvable(msg)) return;
claimAgentTarget(msg.targetId, { eligible: true }).then((claim) => {
if (claim.granted) { noteAgentTarget(msg.targetId, 'acting'); actOnAgentTarget(msg); return; }
noteAgentTarget(msg.targetId, 'denied');
if (!claim.pending) return;
setTimeout(() => claimAndActOnAgentTarget(msg), AGENT_TARGET_RESCUE_RETRY_MS);
});
}
function retryDeclinedAgentTargets() {
if (busyDeclinedTargets.size === 0 || agentTargetBusyReason()) return;
for (const [targetId, msg] of busyDeclinedTargets) {
busyDeclinedTargets.delete(targetId);
claimAndActOnAgentTarget(msg);
}
}
// This page's participation in each target it heard: 'acting' once a
// claim was granted, 'done' once it replied (or stood down from a lapsed
// lease), else the word it last gave. The server replays pending targets
// to every connection that opens. After a reconnect that overlapped the
// old connection the server still holds this page's word; after one that
// did not, it dropped the word on the close, so a replayed target is
// handled again: a busy or unresolvable page re-declines (idempotent), an
// idle page claims.
const agentTargetsSeen = new Map();
function noteAgentTarget(targetId, status) {
agentTargetsSeen.set(targetId, status);
if (agentTargetsSeen.size > 100) agentTargetsSeen.delete(agentTargetsSeen.keys().next().value);
}
// A target this page took a lease on is off-limits for a replay: while
// acting (a second claim or Go), and once done, because its result may
// still be on the wire and this tab is GENERATING by then, so handling
// the replay would decline busy, hand the lease back mid-resolution, and
// let another tab fire a second Go.
function agentTargetTaken(targetId) {
const status = agentTargetsSeen.get(targetId);
return status === 'acting' || status === 'done';
}
// Only a page that can resolve the target claims it. A tab whose page
// lacks the element declines with its resolution verdict instead, so a
// first-wins claim never lets the wrong page answer for a target that
// another page has. The server prefers a busy report (a tab that could
// serve later) over these, and returns the resolution verdict only when
// no connected page can serve.
//
// An element can be momentarily absent (a route still rendering, an HMR
// commit mid-swap), so a failed resolution is not this page's final word:
// it is re-checked a few times over about two seconds, claiming the
// moment the element mounts, and only the last miss is reported. The
// server's timeout still bounds the whole exchange.
// The page reports the miss at once (so the other overlays' words can
// complete the roll call) and keeps re-checking at this cadence for as
// long as the server says the request is pending: the server holds an
// all-no_match roll call open for a short grace precisely so a late mount
// can still be claimed, drops the stale report on an eligible claim, and
// ends the watch by answering pending:false once the request resolved or
// timed out.
const AGENT_TARGET_RESOLVE_WATCH_MS = 500;
function declineAgentTargetUnresolvable(msg) {
const probe = resolveAgentTargetElement(msg);
if (!probe.error) return false;
reportAgentTargetUnresolvable(msg, probe.error);
return true;
}
function reportAgentTargetUnresolvable(msg, error) {
noteAgentTarget(msg.targetId, 'declined');
claimAgentTarget(msg.targetId, { eligible: false, state, reason: 'no_match', result: error }).then((answer) => {
if (!answer.pending) return;
setTimeout(() => watchAgentTargetResolution(msg, error), AGENT_TARGET_RESOLVE_WATCH_MS);
});
}
function watchAgentTargetResolution(msg, lastError) {
if (agentTargetOverlayGone() || agentTargetTaken(msg.targetId)) return;
const busy = agentTargetBusyReason(msg.targetId);
if (busy) { declineAgentTargetBusy(msg, busy); return; }
const probe = resolveAgentTargetElement(msg);
if (!probe.error) { claimAndActOnAgentTarget(msg); return; }
// Still unresolvable: re-report (idempotent); the answer says whether
// the server is still holding the request open.
reportAgentTargetUnresolvable(msg, probe.error || lastError);
}
function handleAgentTarget(msg) {
if (!msg || typeof msg.targetId !== 'string') return;
if (agentTargetTaken(msg.targetId)) return;
noteAgentTarget(msg.targetId, 'heard');
const busy = agentTargetBusyReason(msg.targetId);
if (busy) {
// Roll call: a busy tab reports itself and never acts. The server
// answers `busy` the moment every connected overlay has reported, so
// an idle tab elsewhere is never raced by a timer.
declineAgentTargetBusy(msg, busy);
return;
}
if (declineAgentTargetUnresolvable(msg)) return;
// Eligible tabs race for the server's lease and only the holder acts. A
// hidden tab yields a short head start so a visible one wins when both
// exist, and still serves the request on its own: the user finds the
// selection waiting when they return to it.
setTimeout(() => claimAndActOnAgentTarget(msg), document.hidden ? 150 : 0);
}
function actOnAgentTarget(msg) {
if (agentTargetOverlayGone()) return;
// Every exit ends this tab's acting state, so a later target is not
// refused for a Go that already happened or never will.
const reply = (result) => { noteAgentTarget(msg.targetId, 'done'); postAgentTargetResult(msg.targetId, result); };
const busy = agentTargetBusyReason(msg.targetId);
if (busy) {
// Turned busy between claim and act: report it, which also hands the
// lease back so the roll call can complete or a rescuer can claim.
declineAgentTargetBusy(msg, busy);
return;
}
const resolved = resolveAgentTargetElement(msg);
if (resolved.error) {
// The element went away between claim and act. A result would end the
// request for every tab; a decline hands the lease back so another
// page or a remount can still serve it.
reportAgentTargetUnresolvable(msg, resolved.error);
return;
}
const el = resolved.el;
if (msg.dryRun) {
reply({
ok: true,
dryRun: true,
matchCount: resolved.matchCount,
element: describeAgentTargetCandidate(el),
});
return;
}
scrollAgentTargetIntoView(el, () => {
// Torn down during the scroll settle: do not renew. The lease lapses
// for a rescuer instead of Go minting a session on a dismantled
// overlay.
if (agentTargetOverlayGone()) return;
// Renew the lease right before the irreversible part: a tab whose
// lease lapsed while it scrolled (a rescuer took over) stops here, so
// one request never gets two Go presses.
claimAgentTarget(msg.targetId, { eligible: true }).then((renewal) => {
if (!renewal.granted) { noteAgentTarget(msg.targetId, 'done'); return; }
// An insert placement left mid-configure gives way, exactly as a
// click outside it does in handleClick.
if (state === 'CONFIGURING' && configureKind === 'insert') cancelInsertConfigure();
// Mirror of the user-click pick entry in handleClick, minus the
// pick-mode gate (the agent's intent replaces the toggle); the entry
// goes through beginNewLiveConfiguration like every other pick so
// deferred recovery sees a fresh interaction revision.
selectedElement = el;
beginNewLiveConfiguration();
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
showBar('configure');
editBadgeSuppressed = true;
renderEditBadge('hidden');
startScrollTracking();
maybePrefetchPage();
maybeWarnConditionalAncestor(selectedElement);
// Preset what the agent asked for, then fire the same Go a user press
// fires. handleGo reads exactly these inputs.
selectedAction = msg.action;
selectedCount = msg.count;
// updateBarContent rebuilds the configure row and replaces the input
// element, so the prompt must be written into the input it creates,
// never before (the action-chip click handler does the same dance).
updateBarContent('configure');
const input = uiGetById(PREFIX + '-input');
if (input) input.value = msg.prompt || '';
// The target rides on the generate event too: the helper resolves
// the request from whichever lands first, so a page that dies
// between Go and its result cannot leave the request pending for a
// second Go elsewhere.
const candidate = describeAgentTargetCandidate(el);
agentTargetForGo = { targetId: msg.targetId, matchCount: resolved.matchCount, action: msg.action, count: msg.count, element: candidate };
handleGo();
agentTargetForGo = null;
if (state === 'GENERATING' && currentSessionId) {
reply({
ok: true,
matchCount: resolved.matchCount,
sessionId: currentSessionId,
action: msg.action,
count: msg.count,
element: candidate,
});
} else {
reply({ ok: false, error: 'go_failed', state });
}
});
});
}
// SSE (server→browser) + fetch POST (browser→server) // SSE (server→browser) + fetch POST (browser→server)
// Zero-dependency replacement for WebSocket. // Zero-dependency replacement for WebSocket.
// //
@@ -7121,7 +7538,7 @@
const SSE_MAX_RETRIES = 20; // generous: heartbeats keep the connection alive, so retries mean real trouble const SSE_MAX_RETRIES = 20; // generous: heartbeats keep the connection alive, so retries mean real trouble
function connectSSE() { function connectSSE() {
evtSource = new EventSource('http://localhost:' + PORT + '/events?token=' + TOKEN); evtSource = new EventSource('http://localhost:' + PORT + '/events?token=' + TOKEN + '&clientId=' + AGENT_TARGET_CLIENT_ID);
evtSource.onopen = () => { evtSource.onopen = () => {
sseRetries = 0; // reset on successful (re)connect sseRetries = 0; // reset on successful (re)connect
@@ -7132,8 +7549,11 @@
let msg; try { msg = JSON.parse(e.data); } catch { return; } let msg; try { msg = JSON.parse(e.data); } catch { return; }
switch (msg.type) { switch (msg.type) {
case 'connected': case 'connected':
applyLiveBarPreference(msg.hideLiveBar === true);
hasProjectContext = !!msg.hasProjectContext; hasProjectContext = !!msg.hasProjectContext;
if (!hasProjectContext) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000); // The generate lane runs without PRODUCT.md by design and never
// sends the user to init, so its quiet chrome skips this notice.
if (!hasProjectContext && !liveBarHiddenByHelper) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000);
console.log('[impeccable] Live mode connected.'); console.log('[impeccable] Live mode connected.');
syncAgentPollingUi(!!msg.agentPolling); syncAgentPollingUi(!!msg.agentPolling);
startAgentStatusPoll(); startAgentStatusPoll();
@@ -7143,9 +7563,15 @@
syncPageInteractionCursor(); syncPageInteractionCursor();
syncPageChatFocus('sse-connected'); syncPageChatFocus('sse-connected');
break; break;
case 'live_bar':
applyLiveBarPreference(msg.hidden === true);
break;
case 'agent_polling': case 'agent_polling':
syncAgentPollingUi(!!msg.connected); syncAgentPollingUi(!!msg.connected);
break; break;
case 'agent_target':
handleAgentTarget(msg);
break;
case 'agent_phase': case 'agent_phase':
if (msg.id === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) { if (msg.id === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) {
// Advance the visible phase monotonically. A behind/resumed // Advance the visible phase monotonically. A behind/resumed
@@ -7208,6 +7634,11 @@
disableInlineEdit(); disableInlineEdit();
refreshParamsPanel(); refreshParamsPanel();
} }
// The done reply is the agent's last word on this generation:
// with every variant mounted and no knobs declared, the Tune
// chip must stop spinning. A reload between the mount and this
// reply restored the pending state from the cache.
completeParameterGenerationIfReady();
break; break;
} }
// Source fallback when HMR did not land variants in this tab. // Source fallback when HMR did not land variants in this tab.
@@ -7371,6 +7802,15 @@
}).then(async res => { }).then(async res => {
if (res.ok) return res; if (res.ok) return res;
const body = await res.json().catch(() => ({})); const body = await res.json().catch(() => ({}));
// The helper refused to open a session for an agent target it has
// already answered (another page served it after this page's lease
// lapsed mid-capture, or the request timed out): drop the local
// session and hand the surface back.
if (body.error === 'agent_target_already_served' && msg.type === 'generate'
&& msg.id && msg.id === currentSessionId) {
abandonSupersededGo(msg.id);
return null;
}
// The server refused to journal progress for a session it has never // The server refused to journal progress for a session it has never
// seen: this browser is carrying state from another project or a // seen: this browser is carrying state from another project or a
// wiped store (two apps sharing a localhost port). Continuing to // wiped store (two apps sharing a localhost port). Continuing to
@@ -7392,6 +7832,14 @@
return sessionCreationGate.then(doSend); return sessionCreationGate.then(doSend);
} }
function abandonSupersededGo(sessionId) {
if (sessionId !== currentSessionId) return;
console.warn('[impeccable] The helper already answered this agent target; clearing session ' + sessionId + '.');
markSessionHandled();
cleanup({ instantChrome: true });
showToast('The helper already answered this request, so this session was cleared. Pick an element to start fresh.', 6000);
}
let abandonedForeignSessionId = null; let abandonedForeignSessionId = null;
function abandonForeignSession(sessionId) { function abandonForeignSession(sessionId) {
if (abandonedForeignSessionId === sessionId || sessionId !== currentSessionId) return; if (abandonedForeignSessionId === sessionId || sessionId !== currentSessionId) return;
@@ -7796,6 +8244,7 @@
visibleVariant = 0; visibleVariant = 0;
generationPhase = 'queued'; generationPhase = 'queued';
parameterGenerationState = 'pending'; parameterGenerationState = 'pending';
sessionOrigin = agentTargetForGo ? 'agent' : null;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
resetSessionFileMeta(); resetSessionFileMeta();
@@ -7821,6 +8270,24 @@
}; };
if (snapshot.comments.length > 0) basePayload.comments = snapshot.comments; if (snapshot.comments.length > 0) basePayload.comments = snapshot.comments;
if (snapshot.strokes.length > 0) basePayload.strokes = snapshot.strokes; if (snapshot.strokes.length > 0) basePayload.strokes = snapshot.strokes;
if (agentTargetForGo) {
// An agent-initiated Go names the target it serves (see
// actOnAgentTarget): the helper resolves that request from this event
// as well as from the overlay's own result post.
basePayload.agentTarget = {
targetId: agentTargetForGo.targetId,
clientId: AGENT_TARGET_CLIENT_ID,
result: {
ok: true,
matchCount: agentTargetForGo.matchCount,
sessionId: currentSessionId,
action: agentTargetForGo.action,
count: agentTargetForGo.count,
element: agentTargetForGo.element,
},
};
agentTargetForGo = null;
}
// Hide the interactive overlay so it doesn't linger during generation. // Hide the interactive overlay so it doesn't linger during generation.
hideAnnotOverlay(); hideAnnotOverlay();
@@ -7881,6 +8348,7 @@
visibleVariant = 0; visibleVariant = 0;
generationPhase = 'queued'; generationPhase = 'queued';
parameterGenerationState = 'pending'; parameterGenerationState = 'pending';
sessionOrigin = agentTargetForGo ? 'agent' : null;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
resetSessionFileMeta(); resetSessionFileMeta();
selectedElement = placeholderElement; selectedElement = placeholderElement;
@@ -8927,6 +9395,8 @@ void main() {
pagePickSkipClick = false; pagePickSkipClick = false;
currentSessionId = null; currentSessionId = null;
parameterGenerationState = 'idle'; parameterGenerationState = 'idle';
sessionOrigin = null;
editBadgeSuppressed = false;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
selectedAction = 'impeccable'; selectedAction = 'impeccable';
pendingAcceptedSession = null; pendingAcceptedSession = null;
@@ -9018,6 +9488,7 @@ void main() {
paramsCurrentValues = { ...saved.paramValues }; paramsCurrentValues = { ...saved.paramValues };
} }
if (saved.parameterState) parameterGenerationState = saved.parameterState; if (saved.parameterState) parameterGenerationState = saved.parameterState;
sessionOrigin = saved.origin === 'agent' ? 'agent' : null;
if (saved.generationPhase) generationPhase = saved.generationPhase; if (saved.generationPhase) generationPhase = saved.generationPhase;
} }
@@ -9105,7 +9576,12 @@ void main() {
} }
function restoreSessionWithoutWrapper(reason, activeSessions) { function restoreSessionWithoutWrapper(reason, activeSessions) {
const cached = loadSession(); // The session cache is per origin, so a tab on another page of the same
// app sees this page's session too. Only the page that saved it may
// resume it: the server-adoption branch below already applies the same
// check, and a tab on another page has nothing to render for it.
const cachedRaw = loadSession();
const cached = cachedRaw?.id && !pageMatchesCurrent(cachedRaw.pageUrl) ? null : cachedRaw;
// localStorage is a cache, not a gate. A cleared tab, a second browser // localStorage is a cache, not a gate. A cleared tab, a second browser
// profile, or a teardown that dropped local state all leave the durable // profile, or a teardown that dropped local state all leave the durable
// server session as the only record of work in progress; adopt it instead // server session as the only record of work in progress; adopt it instead
@@ -9218,6 +9694,7 @@ void main() {
pageUrl: location.pathname, pageUrl: location.pathname,
paramValues: { ...paramsCurrentValues }, paramValues: { ...paramsCurrentValues },
parameterState: parameterGenerationState, parameterState: parameterGenerationState,
origin: sessionOrigin || undefined,
insertPlaceholder: insertPlaceholderSnapshot || undefined, insertPlaceholder: insertPlaceholderSnapshot || undefined,
pickedAnchor: pickedAnchorSnapshot || undefined, pickedAnchor: pickedAnchorSnapshot || undefined,
pickedAnchorViewportTop: Number.isFinite(pickedAnchorViewportTop) ? pickedAnchorViewportTop : undefined, pickedAnchorViewportTop: Number.isFinite(pickedAnchorViewportTop) ? pickedAnchorViewportTop : undefined,
@@ -9343,6 +9820,8 @@ void main() {
pagePickSkipClick = false; pagePickSkipClick = false;
currentSessionId = null; currentSessionId = null;
parameterGenerationState = 'idle'; parameterGenerationState = 'idle';
sessionOrigin = null;
editBadgeSuppressed = false;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
selectedAction = 'impeccable'; selectedAction = 'impeccable';
renderEditBadge('hidden'); renderEditBadge('hidden');
@@ -9601,6 +10080,14 @@ void main() {
const resumedState = arrivedVariants > 0 ? 'CYCLING' : 'GENERATING'; const resumedState = arrivedVariants > 0 ? 'CYCLING' : 'GENERATING';
// A reload between the variants mounting and the agent's done reply
// restores a pending Tune state from the cache; the helper knows whether
// that generation already finished.
if (arrivedVariants >= expectedVariants && expectedVariants > 0
&& (parameterGenerationState === 'pending' || parameterGenerationState === 'loading')) {
settleParameterStateFromHelper(sessionId);
}
// Find the visible variant's content element for highlight positioning. // Find the visible variant's content element for highlight positioning.
const isInsert = wrapper.dataset.impeccableMode === 'insert'; const isInsert = wrapper.dataset.impeccableMode === 'insert';
const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null; const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null;
@@ -11065,6 +11552,21 @@ void main() {
} }
} }
// After a resume the cache may say the Tune knobs are still coming while
// the agent already replied done before the reload. The helper's session
// record settles it; otherwise the done reply on SSE does.
function settleParameterStateFromHelper(sessionId) {
fetch('http://localhost:' + PORT + '/status?token=' + TOKEN, { cache: 'no-store' })
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
if (!data || sessionId !== currentSessionId) return;
const session = (data.activeSessions || []).find((s) => s && s.id === sessionId);
if (!session) return;
if (session.generationCompletedAt || session.generationPhase === 'completed') completeParameterGenerationIfReady();
})
.catch(() => { /* the done reply on SSE settles it otherwise */ });
}
function fetchAgentPollingStatus() { function fetchAgentPollingStatus() {
fetch('http://localhost:' + PORT + '/status?token=' + TOKEN, { cache: 'no-store' }) fetch('http://localhost:' + PORT + '/status?token=' + TOKEN, { cache: 'no-store' })
.then((res) => (res.ok ? res.json() : null)) .then((res) => (res.ok ? res.json() : null))
@@ -11104,11 +11606,15 @@ void main() {
uiAppendStyle(s); uiAppendStyle(s);
} }
// The generate lane's helper says so in the served script itself, so a
// lane session never draws the bar at all; every other session mounts
// it exactly as before.
const barHiddenFromStart = window.__IMPECCABLE_LIVE_BAR_HIDDEN__ === true;
globalBarEl = el('div', { globalBarEl = el('div', {
position: 'fixed', bottom: '14px', left: '50%', position: 'fixed', bottom: '14px', left: '50%',
transform: 'translateX(-50%) translateY(20px)', transform: 'translateX(-50%) translateY(20px)',
zIndex: Z.bar + 5, zIndex: Z.bar + 5,
display: 'flex', alignItems: 'stretch', display: barHiddenFromStart ? 'none' : 'flex', alignItems: 'stretch',
gap: '0', gap: '0',
width: 'max-content', width: 'max-content',
background: P.surface, background: P.surface,
@@ -11124,6 +11630,10 @@ void main() {
}); });
globalBarEl.id = PREFIX + '-global-bar'; globalBarEl.id = PREFIX + '-global-bar';
globalBarEl.dataset.theme = theme; globalBarEl.dataset.theme = theme;
if (barHiddenFromStart) {
liveBarHiddenByHelper = true;
globalBarEl.dataset.liveBarDisplay = 'flex';
}
// Brand mark - kinpaku Impeccable icon (site header / favicon paths). // Brand mark - kinpaku Impeccable icon (site header / favicon paths).
const brand = el('span', { const brand = el('span', {
@@ -11519,6 +12029,9 @@ void main() {
// Listen for detection results AND ready signal // Listen for detection results AND ready signal
window.addEventListener('message', onDetectMessage); window.addEventListener('message', onDetectMessage);
updateGlobalBarState(); updateGlobalBarState();
// The helper may already have said the bar stays hidden (a connect
// that raced the bar build, or a reload mid-lane): re-apply it here.
if (liveBarHiddenByHelper) setLiveBarHidden(true);
} }
function updateGlobalBarState() { function updateGlobalBarState() {
@@ -11715,6 +12228,13 @@ void main() {
/** Full teardown: remove all UI, disconnect SSE, clean up. */ /** Full teardown: remove all UI, disconnect SSE, clean up. */
function teardown() { function teardown() {
// Declined targets die with the overlay: the IDLE transition below must
// not re-claim a lease this page can no longer act on. So does the
// target ledger: an 'acting' entry from a Go that never happened must
// not refuse every target the next connection hears.
busyDeclinedTargets.clear();
agentTargetsSeen.clear();
liveBarHiddenByHelper = false;
stopAgentStatusPoll(); stopAgentStatusPoll();
hideAgentPollTooltip(); hideAgentPollTooltip();
if (agentPollTooltipEl) { if (agentPollTooltipEl) {
+3 -2
View File
@@ -2,7 +2,7 @@
name: impeccable name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks. description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
metadata: metadata:
version: 4.4.0 version: 4.3.1
--- ---
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft. This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
@@ -63,7 +63,8 @@ Choose the mode from the requested surface, not the product, and persist it only
| `clarify [target]` | Fix | Improve UX copy, labels, and error messages | [reference/clarify.md](reference/clarify.md) | | `clarify [target]` | Fix | Improve UX copy, labels, and error messages | [reference/clarify.md](reference/clarify.md) |
| `adapt [target]` | Fix | Adapt for different devices and screen sizes | [reference/adapt.md](reference/adapt.md) · native: [reference/adapt.native.md](reference/adapt.native.md) | | `adapt [target]` | Fix | Adapt for different devices and screen sizes | [reference/adapt.md](reference/adapt.md) · native: [reference/adapt.native.md](reference/adapt.native.md) |
| `optimize [target]` | Fix | Diagnose and fix UI performance | [reference/optimize.md](reference/optimize.md) | | `optimize [target]` | Fix | Diagnose and fix UI performance | [reference/optimize.md](reference/optimize.md) |
| `live` | Iterate | Visual variant mode: pick elements in the browser, generate alternatives | [reference/live.md](reference/live.md) | | `live` | Iterate | Visual variant mode: pick elements in the browser, iterate on alternatives | [reference/live.md](reference/live.md) |
| `generate [n] [action] [element]` | Iterate | Variants, versions, or alternatives of a named element to choose from in the live browser; no manual picking | [reference/generate.md](reference/generate.md) |
Routing: Routing:
@@ -15,10 +15,6 @@ Do not redesign. Preserve the reference's visual role, silhouette, palette, ligh
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 `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; 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; report a card too thin to brief a comp, never pad it 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 (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 keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. 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. 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 `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; 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; report a card too thin to brief a comp, never pad it 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 (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 keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. 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.
## Review handoff
After the initial production batch, return the actual files and any unresolved drift to the parent for the user-facing component review in [component-review.md](../reference/component-review.md). The parent includes rendered code regions alongside these rasters. A parent or automatic visual check is not a substitute for that human checkpoint. Apply requested repairs and preserve unchanged assets; do not self-approve them. This checkpoint does not apply to the Decision Comps job above.
## Input Contract ## Input Contract
Expect the measured spec (`.impeccable/build/spec.json`, written by `impeccable comp-spec` from the approved comp), the approved comp path, and the skill scripts path. Optionally: a subset of region ids to produce, extra prompt notes per region, and format or transparency needs. Everything else you need is in the spec: each raster region's id, kind (plate, image, texture), pixel box, sampled palette, aspect, note, and the plate path it must land on. Expect the measured spec (`.impeccable/build/spec.json`, written by `impeccable comp-spec` from the approved comp), the approved comp path, and the skill scripts path. Optionally: a subset of region ids to produce, extra prompt notes per region, and format or transparency needs. Everything else you need is in the spec: each raster region's id, kind (plate, image, texture), pixel box, sampled palette, aspect, note, and the plate path it must land on.
@@ -18,7 +18,7 @@ Expect: the original request; the confirmed user answers; the artifact path(s);
## Checks, in order ## Checks, in order
0. **Evidence.** Before any other check, verify the required captures exist and every capture is valid. Required: the platform's full viewport set (web: `desktop.png` and `mobile.png`; native: one capture per shipped device class), plus every capture the calling brief names as required, a reported user viewport (`user-<width>.png`) included. Valid: no black or blank regions, content matching what the filename claims (a visit capture showing the About section is invalid), the document top visible where the file claims a full page, dimensions that make sense for the named viewport. A required capture that is absent fails exactly like one that is malformed: a viewport nobody captured is a viewport nobody inspected, and it cannot ship. When any capture fails, the whole review changes shape: return `disposition: recapture` as the first line, then one section, `recapture`, listing each missing or invalid file and what a valid capture of it shows, and stop. Never build a matrix on malformed evidence; a verdict derived from a broken capture launders the breakage into an approval, and the parent owes you a full re-review on valid captures, not a scoring round. 0. **Evidence.** Before any other check, verify the required captures exist and every capture is valid. Required: the platform's full viewport set (web: `desktop.png` and `mobile.png`; native: one capture per shipped device class), plus every capture the calling brief names as required, a reported user viewport (`user-<width>.png`) included. Valid: no black or blank regions, content matching what the filename claims (a visit capture showing the About section is invalid), the document top visible where the file claims a full page, dimensions that make sense for the named viewport. A required capture that is absent fails exactly like one that is malformed: a viewport nobody captured is a viewport nobody inspected, and it cannot ship. When any capture fails, the whole review changes shape: return `disposition: recapture` as the first line, then one section, `recapture`, listing each missing or invalid file and what a valid capture of it shows, and stop. Never build a matrix on malformed evidence; a verdict derived from a broken capture launders the breakage into an approval, and the parent owes you a full re-review on valid captures, not a scoring round.
1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and every phase before `review` is `closed` or explicitly `skipped`; an open or failed phase is a material finding. A comp-led config with no state file, or a state whose `comps` phase is neither `closed` nor `skipped`, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. 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, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all. 1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and its `comps` (or `skipped` when a surface round locked the comp), `spec`, `plates`, and `hero` phases are `closed`; a comp-led config with no state file, or a state whose `comps` phase never closed, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. 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, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all.
2. **Fidelity.** Start from the measurement, then judge what it cannot: read `.impeccable/review/diff/final/report.json` (and hero) first; every region scored `missing` or `contradicted` is a matrix row in that state unless the paired crop under `regions/` shows the score is wrong, and you say why; a region scored `match` still gets your eye for lettering character and material, which the numbers do not measure. Then, 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; 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. Three 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; medium is part of the promise. GROUND: the page field's value and temperature against the comp's, sampled from pixels on both sides when tooling allows rather than judged from memory, and read as the net on-screen result where a texture or tile paints over the base color; a ground warmer or cooler than the comp's is contradicted however faithfully the layout matches, and drift toward the rendition prior (warm cream on light grounds, blue-black slate on dark) is the direction to hunt. With no approved comp, 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 renders) as contradicted on its face; imitation material is the single most reliable mark of machine-made design. GROUND narrows rather than lapses: with no comp to sample, a color OWN-WORLD names is the target and the same warmer-or-cooler judgment applies; when OWN-WORLD names none, there is no GROUND authority, and the review says so in place of a verdict, because a target the reviewer invents turns the check into taste. A critique-reference comp on such a build is provocation, not spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is what the image dared that the build did not, and 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. A fix that requires producing an asset says so explicitly ("produce: <region> 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. 2. **Fidelity.** Start from the measurement, then judge what it cannot: read `.impeccable/review/diff/final/report.json` (and hero) first; every region scored `missing` or `contradicted` is a matrix row in that state unless the paired crop under `regions/` shows the score is wrong, and you say why; a region scored `match` still gets your eye for lettering character and material, which the numbers do not measure. Then, 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; 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. Three 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; medium is part of the promise. GROUND: the page field's value and temperature against the comp's, sampled from pixels on both sides when tooling allows rather than judged from memory, and read as the net on-screen result where a texture or tile paints over the base color; a ground warmer or cooler than the comp's is contradicted however faithfully the layout matches, and drift toward the rendition prior (warm cream on light grounds, blue-black slate on dark) is the direction to hunt. With no approved comp, 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 renders) as contradicted on its face; imitation material is the single most reliable mark of machine-made design. GROUND narrows rather than lapses: with no comp to sample, a color OWN-WORLD names is the target and the same warmer-or-cooler judgment applies; when OWN-WORLD names none, there is no GROUND authority, and the review says so in place of a verdict, because a target the reviewer invents turns the check into taste. A critique-reference comp on such a build is provocation, not spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is what the image dared that the build did not, and 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. A fix that requires producing an asset says so explicitly ("produce: <region> 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. 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, 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. 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, 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.
@@ -188,6 +188,12 @@ Test thoroughly across contexts:
- **Edge cases**: Very small screens (320px), very large screens (4K) - **Edge cases**: Very small screens (320px), very large screens (4K)
- **Slow connections**: Test on throttled network - **Slow connections**: Test on throttled network
**Custom controls** (sliders, drag surfaces, scrollable control strips): a before/after slider can pass every width check above and still refuse to drag on iOS, so exercise each one in scope in the same batched round as the checks above:
- **Primary gesture**: Tap it and confirm it responds as designed, then drag it with the target input method; the drag must complete, not just start
- **Scroll across it**: A swipe along the page's scroll axis across the control scrolls the page or container without activating it; a drag that starts on the control along its axis moves the control, not the page. Neither failure throws an error, so try both
- **Evidence**: Say what produced the evidence: an emulated viewport, synthesized touch input through a browser tool, which engine ran it (Chromium is not Safari), or a physical device. Screenshots and resized viewports verify layout, never a gesture. Name what stayed untested and move on; unreachable hardware is a reported gap, not a blocker
When the adaptation feels native to each context, hand off to `$impeccable polish` for the final pass. When the adaptation feels native to each context, hand off to `$impeccable polish` for the final pass.
--- ---
+2 -1
View File
@@ -48,11 +48,12 @@ Run comprehensive checks across 5 dimensions. Score each dimension 0-4 using the
**Check for**: **Check for**:
- **Fixed widths**: Hard-coded widths that break on mobile - **Fixed widths**: Hard-coded widths that break on mobile
- **Touch targets**: Interactive elements < 44x44px - **Touch targets**: Interactive elements < 44x44px
- **Broken touch interaction**: Custom sliders, drag surfaces, and scrollable control strips whose primary gesture fails under touch, that swallow page scroll or lose the drag to it, or that stay stuck after an interrupted gesture. Code tells: mouse-only handlers, no `touch-action` on a pointer-event drag surface, drag state that nothing clears on cancel, lost capture, or blur. Exercise the gesture when a browser tool can synthesize touch (a rendered viewport proves layout, not the gesture), then say what produced the evidence (emulated viewport, synthesized touch, which engine, physical device) and what stayed untested
- **Horizontal scroll**: Content overflow on narrow viewports - **Horizontal scroll**: Content overflow on narrow viewports
- **Text scaling**: Layouts that break when text size increases - **Text scaling**: Layouts that break when text size increases
- **Missing breakpoints**: No mobile/tablet variants - **Missing breakpoints**: No mobile/tablet variants
**Score 0-4**: 0=Desktop-only (breaks on mobile), 1=Major issues (some breakpoints, many failures), 2=Partial (works on mobile, rough edges), 3=Good (responsive, minor touch target or overflow issues), 4=Excellent (fluid, all viewports, proper touch targets) **Score 0-4**: 0=Desktop-only (breaks on mobile), 1=Major issues (some breakpoints, many failures), 2=Partial (works on mobile, rough edges), 3=Good (responsive, minor touch target or overflow issues), 4=Excellent (fluid, all viewports, proper touch targets, gestures work under touch)
### 5. Implementation Integrity (CRITICAL) ### 5. Implementation Integrity (CRITICAL)
@@ -1,57 +0,0 @@
# Component review
Use this checkpoint on comp-led builds after producing the initial component kit and before composing the page. The approved comp is the reference. The user reviews the actual produced components, including code; a list of planned assets or screenshots supplied by the builder is not a review of what will ship.
## Prepare the component kit
Keep the measured spec's region IDs. Include every visible region: produced raster assets and working HTML/CSS/SVG for text, controls, patterns, decoration and layout elements. A region rendered in code needs an actual review document, not a promise to implement it later. Use semantic HTML for content and controls. Do not flatten the page or combine unrelated regions to avoid review. Report omitted regions so the user can mark what is missing.
Write `.impeccable/review/components.json` with this manifest format:
```json
{
"schemaVersion": 1,
"id": "components",
"title": "Component review",
"stage": "components",
"comp": {"path": ".impeccable/mocks/comp-2.png", "width": 1536, "height": 1024},
"components": [
{
"id": "illustration",
"name": "Illustration",
"medium": "raster",
"box": {"x": 0.5, "y": 0.2, "w": 0.45, "h": 0.7},
"note": "Produced cutout; positioned over the page ground.",
"preview": {"kind": "image", "path": "assets/illustration.png"},
"dependencies": [".impeccable/build/spec.json"]
},
{
"id": "headline",
"name": "Headline",
"medium": "html",
"box": {"x": 0.05, "y": 0.2, "w": 0.4, "h": 0.25},
"note": "Rendered semantic heading and its typography.",
"preview": {"kind": "page", "path": ".impeccable/review/components/headline.html"},
"dependencies": [".impeccable/build/spec.json", "assets/type.woff2"]
}
]
}
```
The coordinates above only illustrate the schema. Use the approved comp's actual pixel dimensions and each measured region's normalized bounds (`x / width`, `y / height`, `w / width`, `h / height`). A code preview is rendered at the comp viewport and cropped to that component's box, so place its content at those coordinates in the review document. Include every file the document uses in `dependencies`, including linked CSS, fonts and images. The runtime also binds the measured spec for the component stage and checks its inventory. Local paths only. Static PNG, WebP and JPEG previews retain their original bytes and actual transparency; never draw a checkerboard into the asset.
Native capture supports stable HTML/CSS and inline SVG. Supply a static review state for motion and keep the implementation's real inputs. A scripted, canvas or otherwise unsupported component is a blocker to report, not permission to substitute a raster or omit it.
## Present and wait
If the harness exposes `component_review`, call it with `manifest_path` set to `.impeccable/review/components.json`. The host captures the component files, presents this same review interface and returns the user's decisions. A suspended request is waiting for the user; it is not a failed build or an approval.
Otherwise run `.agents/skills/impeccable/scripts/impeccable component-review capture --manifest .impeccable/review/components.json`, then start `.agents/skills/impeccable/scripts/impeccable component-review serve --session <returned session>` in the background. Open the URL it prints in the available browser and wait for the user. Read the result with `.agents/skills/impeccable/scripts/impeccable component-review verify --manifest .impeccable/review/components.json`; pending, needs-work and stale input all refuse approval. Never submit the page or write a receipt on the user's behalf.
The user can approve components, request changes, and mark missing regions. Act on their feedback without replacing it with your own favorable verdict. Keep component IDs stable, update the actual implementation and dependency list, and present another round. The UI carries only approvals whose component inputs have not changed. Do not ask the user to reapprove unchanged work. Continue only when the inventory is confirmed and all components are approved.
## Assemble and review
Build the page from the approved component files. Replacing, simplifying or changing an approved component requires a new component review. Run the existing plates and hero gates; human review does not waive their integrity checks.
After the full page and responsive checks are complete, present a second manifest at `.impeccable/review/hero.json`, with `id` and `stage` set to `hero`. Use one page-preview component covering the assembled first viewport, its real HTML entry, and its complete dependency list. The reference stays the approved comp. Call the same host review tool (or native capture/serve/verify workflow) and obtain the user's approval before the final response. Later edits to the reviewed files require a fresh review. A component-kit approval does not approve their assembled layout.
@@ -13,10 +13,6 @@ Do not redesign. Preserve the reference's visual role, silhouette, palette, ligh
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 `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; 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; report a card too thin to brief a comp, never pad it 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 (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 keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. 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. 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 `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; 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; report a card too thin to brief a comp, never pad it 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 (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 keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. 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.
## Review handoff
After the initial production batch, return the actual files and any unresolved drift to the parent for the user-facing component review in [component-review.md](../reference/component-review.md). The parent includes rendered code regions alongside these rasters. A parent or automatic visual check is not a substitute for that human checkpoint. Apply requested repairs and preserve unchanged assets; do not self-approve them. This checkpoint does not apply to the Decision Comps job above.
## Input Contract ## Input Contract
Expect the measured spec (`.impeccable/build/spec.json`, written by `impeccable comp-spec` from the approved comp), the approved comp path, and the skill scripts path. Optionally: a subset of region ids to produce, extra prompt notes per region, and format or transparency needs. Everything else you need is in the spec: each raster region's id, kind (plate, image, texture), pixel box, sampled palette, aspect, note, and the plate path it must land on. Expect the measured spec (`.impeccable/build/spec.json`, written by `impeccable comp-spec` from the approved comp), the approved comp path, and the skill scripts path. Optionally: a subset of region ids to produce, extra prompt notes per region, and format or transparency needs. Everything else you need is in the spec: each raster region's id, kind (plate, image, texture), pixel box, sampled palette, aspect, note, and the plate path it must land on.
@@ -16,7 +16,7 @@ Expect: the original request; the confirmed user answers; the artifact path(s);
## Checks, in order ## Checks, in order
0. **Evidence.** Before any other check, verify the required captures exist and every capture is valid. Required: the platform's full viewport set (web: `desktop.png` and `mobile.png`; native: one capture per shipped device class), plus every capture the calling brief names as required, a reported user viewport (`user-<width>.png`) included. Valid: no black or blank regions, content matching what the filename claims (a visit capture showing the About section is invalid), the document top visible where the file claims a full page, dimensions that make sense for the named viewport. A required capture that is absent fails exactly like one that is malformed: a viewport nobody captured is a viewport nobody inspected, and it cannot ship. When any capture fails, the whole review changes shape: return `disposition: recapture` as the first line, then one section, `recapture`, listing each missing or invalid file and what a valid capture of it shows, and stop. Never build a matrix on malformed evidence; a verdict derived from a broken capture launders the breakage into an approval, and the parent owes you a full re-review on valid captures, not a scoring round. 0. **Evidence.** Before any other check, verify the required captures exist and every capture is valid. Required: the platform's full viewport set (web: `desktop.png` and `mobile.png`; native: one capture per shipped device class), plus every capture the calling brief names as required, a reported user viewport (`user-<width>.png`) included. Valid: no black or blank regions, content matching what the filename claims (a visit capture showing the About section is invalid), the document top visible where the file claims a full page, dimensions that make sense for the named viewport. A required capture that is absent fails exactly like one that is malformed: a viewport nobody captured is a viewport nobody inspected, and it cannot ship. When any capture fails, the whole review changes shape: return `disposition: recapture` as the first line, then one section, `recapture`, listing each missing or invalid file and what a valid capture of it shows, and stop. Never build a matrix on malformed evidence; a verdict derived from a broken capture launders the breakage into an approval, and the parent owes you a full re-review on valid captures, not a scoring round.
1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and every phase before `review` is `closed` or explicitly `skipped`; an open or failed phase is a material finding. A comp-led config with no state file, or a state whose `comps` phase is neither `closed` nor `skipped`, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. 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, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all. 1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and its `comps` (or `skipped` when a surface round locked the comp), `spec`, `plates`, and `hero` phases are `closed`; a comp-led config with no state file, or a state whose `comps` phase never closed, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. 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, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all.
2. **Fidelity.** Start from the measurement, then judge what it cannot: read `.impeccable/review/diff/final/report.json` (and hero) first; every region scored `missing` or `contradicted` is a matrix row in that state unless the paired crop under `regions/` shows the score is wrong, and you say why; a region scored `match` still gets your eye for lettering character and material, which the numbers do not measure. Then, 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; 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. Three 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; medium is part of the promise. GROUND: the page field's value and temperature against the comp's, sampled from pixels on both sides when tooling allows rather than judged from memory, and read as the net on-screen result where a texture or tile paints over the base color; a ground warmer or cooler than the comp's is contradicted however faithfully the layout matches, and drift toward the rendition prior (warm cream on light grounds, blue-black slate on dark) is the direction to hunt. With no approved comp, 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 renders) as contradicted on its face; imitation material is the single most reliable mark of machine-made design. GROUND narrows rather than lapses: with no comp to sample, a color OWN-WORLD names is the target and the same warmer-or-cooler judgment applies; when OWN-WORLD names none, there is no GROUND authority, and the review says so in place of a verdict, because a target the reviewer invents turns the check into taste. A critique-reference comp on such a build is provocation, not spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is what the image dared that the build did not, and 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. A fix that requires producing an asset says so explicitly ("produce: <region> 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. 2. **Fidelity.** Start from the measurement, then judge what it cannot: read `.impeccable/review/diff/final/report.json` (and hero) first; every region scored `missing` or `contradicted` is a matrix row in that state unless the paired crop under `regions/` shows the score is wrong, and you say why; a region scored `match` still gets your eye for lettering character and material, which the numbers do not measure. Then, 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; 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. Three 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; medium is part of the promise. GROUND: the page field's value and temperature against the comp's, sampled from pixels on both sides when tooling allows rather than judged from memory, and read as the net on-screen result where a texture or tile paints over the base color; a ground warmer or cooler than the comp's is contradicted however faithfully the layout matches, and drift toward the rendition prior (warm cream on light grounds, blue-black slate on dark) is the direction to hunt. With no approved comp, 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 renders) as contradicted on its face; imitation material is the single most reliable mark of machine-made design. GROUND narrows rather than lapses: with no comp to sample, a color OWN-WORLD names is the target and the same warmer-or-cooler judgment applies; when OWN-WORLD names none, there is no GROUND authority, and the review says so in place of a verdict, because a target the reviewer invents turns the check into taste. A critique-reference comp on such a build is provocation, not spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is what the image dared that the build did not, and 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. A fix that requires producing an asset says so explicitly ("produce: <region> 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. 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, 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. 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, 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.
@@ -0,0 +1,101 @@
> **Additional context needed**: only the target element, when the request does not name one that resolves uniquely on the page.
Generate is the fast lane into live mode: the user names an element, a direction, and a count in one sentence, and within a minute they are cycling through variants in their browser. One command boots the helper, hands the element to the overlay in the page your harness already shows (it scrolls to it, selects it, and fires the same Go a click fires) and returns the generate event; one edit writes the variants; one call replies and waits for the user's choice, which the helper bakes into source itself. This file owns the lane's plumbing; from the event onward the design work is [live.md](live.md)'s, unchanged, so read it in full now if you have not this session.
**Web only.** Live mode's browser overlay has no native equivalent; on `ios` / `android` / `adaptive` projects, decline this command and offer `bolder` or `quieter` on the source instead.
The plumbing is where the lane saves time: one command starts the session around the page your harness already shows, one call replies and waits, and nothing here is a browser you have to babysit. The design work is not where it saves time. Setup runs as for any command (`impeccable context`, this reference, craft-floor.md before the edit), and the variants are planned, written, and accepted exactly the way a live session plans, writes, and accepts them.
Three prohibitions cover the known ways this command goes wrong:
- **Never run init or document, and never ask for PRODUCT.md or DESIGN.md.** When they exist, the start command prints them under `boot` and you use them. When they do not, it says so (`contextMissing`, `contextNote`) and you extract the identity from the event (Step 3). A missing file is never a reason to interview the user inside this command; offer `init` in one line after the session ends.
- **Never hand-write a variants wrapper or invent a session id.** Only the browser mints session ids (8 hex characters, at Go). A missing event is fixed by rerunning Step 2, never with a direct source edit.
- **Do not act on hook findings while live markers are in the file**, and do not restyle variants to appease them; the accept verifies the file once the variant is permanent.
## Step 1: Parse the request
Three parts, all from the user's sentence:
- **A number in the request**: that is the count. **No number**: 3. The protocol caps count at 8.
- **The direction wording** maps onto the live action vocabulary; never invent a new action value:
- **bold, bolder, stronger, punchier**: `bolder`
- **quiet, calmer, softer, toned down**: `quieter`
- **simpler, minimal, stripped**: `distill`
- **refined, tightened, polished**: `polish`
- **font and type words**: `typeset`
- **color words**: `colorize`
- **arrangement and spacing words**: `layout`
- **device and breakpoint words**: `adapt`
- **motion words**: `animate`
- **playful words**: `delight`
- **rule-breaking words**: `overdrive`
- **Wording that carries intent but no vocabulary word** ("make it feel like a bank", "warmer", "more premium"): `impeccable`, with the user's wording passed as the prompt.
- **An action fits AND extra intent rides along** ("bolder, but keep it monochrome"): that action, with the rest as the prompt.
- **The wording names no direction at all** ("better", "improve", "nicer", "different", "fresh", "new", "redesign", "fix", "some options", "ideas", "alternatives", or just "variants" with nothing else): STOP and use Codex's structured user-input/question tool when available; if unavailable, ask directly in chat to clarify what you cannot infer. Ask one question, offering the vocabulary: *"Which direction should the variants take? bolder, quieter, simpler (distill), polished, typography (typeset), color (colorize), layout, motion (animate), playful (delight), or rule-breaking (overdrive)."* Map the answer with this list; an answer that is still open ("surprise me", "you pick") is `impeccable` with the user's original wording as the prompt, and Step 2 starts on that answer.
- **The element description** ("the pricing cards", "the hero heading"): Step 2 resolves it to a selector.
Done when you hold an action from the vocabulary (asked for, when the request named no direction), a count from 1 to 8, and the element description.
## Step 2: Reuse the page, then start
**Reuse** the dev server already running and the tab your harness already shows it in; a second server or a second browser window is the failure this step prevents.
1. **Find the dev server**, cheapest source first, and stop at the first hit: the user's message, a browser tab already on the app (Claude Code: an origin in `tabs_context`), a server your harness started (Claude Code: `preview_list`), a terminal that printed its URL. Its origin is your `--dev-url`. **No hit**: leave `--dev-url` off and run the start command with no wait; the boot probes for a running server and its verdict names the move. `browser_needed` carries the `devUrl` it found: open it as in 2, then rerun with `--dev-url <devUrl> --wait-for-browser 60000`. `no_dev_server` means nothing serves the app: start the dev script the way the verdict says (Claude Code: `preview_start`; Cursor: a background terminal; Codex: an exec you yield from), wait for its URL, then rerun with `--dev-url <url>`.
2. **Open the page that renders the element in your browser, then start.** The route the request names, else the one `--target` serves; `--dev-url` takes only the origin.
- **Cursor** (`browser_navigate`) and **Claude Code** (`navigate`, which opens the Browser pane when it is closed and takes the `tabId` from `tabs_context` when a tab is already on that origin): open the URL, then run the start command with `--dev-url <url> --wait-for-browser 60000`. The boot injects the overlay and the page reloads into it while the command waits. Your browser tool is the only opener on these harnesses; the engine ignores `--open` there.
- **No browser tool** (Codex, others): run the start command with `--open --wait-for-browser 120000`; it opens the system browser, and the longer wait covers the user finding the tab. **`browser_open_failed` back**: tell the user the `url` in one line and rerun with `--wait-for-browser 120000`.
```bash
.agents/skills/impeccable/scripts/impeccable live-generate --target src/App.jsx --dev-url http://127.0.0.1:5173/ --selector ".pricing-grid" --action bolder --count 3 --boot --wait-for-browser 60000
```
Run it in the foreground in Cursor and Claude Code (it returns within the wait); on Codex, in an exec you yield from, the way Step 3 runs the poll.
- `--target`: the file that renders the element when the request or the project makes it obvious; skip it otherwise.
- `--dev-url`: the origin from 1; omit it and the boot probes.
- `--selector`: a unique class first, then a landmark tag plus class, an id last (every variant mounts a copy of the element, so an id repeats in the DOM). **The request names a repeated component in plural** ("the pricing cards"): target the container that holds the set, so one scoped stylesheet restyles every instance. One read of the source file that renders the element is allowed when the selector is not obvious; `--dry-run` resolves and reports without starting anything when it is not certain.
- `--boot`: runs the lane's boot (PRODUCT.md and DESIGN.md loaded again for the helper, missing files tolerated, dev URL found, bottom bar hidden for the helper's lifetime) and reuses a helper that is already running. Its result rides along as `boot`.
- Also available: `--prompt`, `--text` (keep only matches whose visible text contains a snippet), `--index` (1-based pick among matches).
Read the output in this order: `boot` (or `boot.contextMissing` with `boot.contextNote`: the page is the source of truth, per the note), then `event`, the generate event for `sessionId`, with the same `_instructions` a user's Go gets. Every verdict carries `_instructions`, and they win over your recollection of this file; the ones whose move is a decision of yours:
- **`ambiguous`**: the candidates are listed; target their common container, or rerun with `--text "<visible text>"` or `--index <n>`.
- **`dev_server_gone`**: the dev server stopped answering while the command waited for the page (on Cursor, a server another chat started dies with that chat). Start it the way the verdict says, then rerun with `--dev-url <url>`.
- **`no_match`**: the tab is on a route that does not render the element (navigate to the right route, rerun), or the selector is wrong (derive a better one from the source, or add `--text`).
- **`config_missing` / `config_invalid`** under `bootError`: follow [live-setup.md](live-setup.md) first, then rerun.
- **`event: null`** with `ok: true`: the event was slower than the wait; run `.agents/skills/impeccable/scripts/impeccable live-poll` once to collect it, then continue.
Done when the output shows `ok: true`, a `sessionId`, and an `event`, reached with at most one server started and one tab opened by you.
## Step 3: Generate
The event is a standard `generate` event: the picked element's context, a preflighted scaffold, and `_instructions` naming the action's reference, the planning section, and the exact splice. Handle it exactly per live.md's **Handle generate**, which owns everything from the identity lock to the done reply: read the action's reference and craft-floor.md as it says, plan per section 4 (identity first, then mode, then three different primary axes, then the squint test), declare knobs per section 7, and deliver per section 6 (a complete replacement of the element per variant, the preview CSS plus every variant in one edit at the scaffold's splice). The lane changes nothing about what a variant may be: the moves a live session would make on this element (a promoted tier, a restructured set, a reordered card, a different surface) are open here too. Never screenshot the page; the overlay preview is the review channel until accept.
**Reply and wait in one call**, with the file you wrote:
```bash
.agents/skills/impeccable/scripts/impeccable live-poll --reply EVENT_ID done --file src/App.jsx --then-poll
```
This replies done (the browser mounts the variants) and then blocks until the user's choice arrives, so run it the way your harness runs a long wait: **Claude Code** in the foreground with your tool's longest timeout (600000 ms), so you are paused until the choice arrives; **Codex** in a yielded foreground exec; **Cursor** in a background terminal with notify on `"type":"(accept|discard|variant_mount_failed|exit)"`. Never pass a short `--timeout=`. While it runs there is nothing else to do: never sleep and never poll its output on a timer; a harness that backgrounds it wakes you when it returns. `{"type":"timeout"}` means the user has not chosen yet: run `live-poll` again and keep waiting. If the edit fails after the browser flipped to GENERATING, `--reply EVENT_ID error "Short reason"` (without `--then-poll`) so the bar resets.
Then tell the user, in one line, where their variants are: *"Three [bolder] variants are live on [the pricing cards]: cycle with the floating bar's arrows, adjust the Tune knobs, and Accept the keeper."*
Outside the replace path, read the matching live.md section before acting: `scaffold.previewMode: "svelte-component"` (Svelte previews are edited as components, and their accept is mechanical), `mode: "insert"`, `variant_mount_failed`, `steer`, `manual_edit_apply`, and any `fallback: "agent-driven"` wrap error.
## Step 4: Accept and close
The call from Step 3 returns the user's choice. **`discard`**: nothing to do. **`accept`**: `_acceptResult.carbonize: true` is the normal case, and the cleanup is live.md's **Required after accept**, unchanged: move the accepted variant's rules into the stylesheet that already owns the element with real selectors, bake the chosen knob values in, unwrap the element and drop every `data-impeccable-*` attribute, delete the inline `<style>` block and both `impeccable-carbonize` markers, then `.agents/skills/impeccable/scripts/impeccable live-complete --id SESSION_ID` and confirm `phase: "completed"`. (`baked: true` appears only when the accept was run with `--bake`; then the helper already made the variant permanent and no `live-complete` is owed.)
Close without being asked, the moment the choice is handled:
```bash
.agents/skills/impeccable/scripts/impeccable live-server stop
```
Stopping removes the injected script and reloads the page once: the user sees the accepted design with no overlay chrome, still served by their dev server. **Never kill or restart the dev server**, including one you started in Step 2.
- **The user asks for more variants before you closed**: skip the close, run Step 2 again for the next element (the helper is reused), and close after the last choice.
- **Interrupted or unsure of the state**: `.agents/skills/impeccable/scripts/impeccable live-status`, then `live-resume`; the journal under `.impeccable/live/sessions/` is canonical.
Done when the helper is stopped and the dev site still answers with the accepted design.
@@ -205,6 +205,11 @@ t('items', { count }) // Handles complex plural rules
- Optimistic updates with rollback - Optimistic updates with rollback
- Conflict resolution - Conflict resolution
**Interrupted gestures** (custom sliders, drag surfaces, scrollable control strips):
- A second finger or pointer lands mid-drag: the first drag keeps its pointer or ends cleanly, never jumps to the new one
- The browser cancels the gesture to scroll (`pointercancel`), capture is lost (`lostpointercapture`), the pointer is released outside the control, or the window loses focus (`blur`) mid-drag: clear the dragging state and release capture
- After each of these, the next tap or drag works without a reload
**Permission states**: **Permission states**:
- No permission to view - No permission to view
- No permission to edit - No permission to edit
@@ -304,6 +309,7 @@ const throttledScroll = throttle(handleScroll, 100);
- Unit tests for edge cases - Unit tests for edge cases
- Integration tests for error scenarios - Integration tests for error scenarios
- E2E tests for critical paths - E2E tests for critical paths
- A behavioral regression for each confirmed gesture fix, when the project's test runner can drive input
- Visual regression tests - Visual regression tests
- Accessibility tests (axe, WAVE) - Accessibility tests (axe, WAVE)
@@ -330,7 +336,10 @@ Test thoroughly with edge cases:
- **Network issues**: Disable internet, throttle connection - **Network issues**: Disable internet, throttle connection
- **Large datasets**: Test with 1000+ items - **Large datasets**: Test with 1000+ items
- **Concurrent actions**: Click submit 10 times rapidly - **Concurrent actions**: Click submit 10 times rapidly
- **Interrupted gestures**: Add a second finger mid-drag, scroll across the control, release outside it, switch windows mid-drag; then drag again
- **Errors**: Force API errors, test all error states - **Errors**: Force API errors, test all error states
- **Empty**: Remove all data, test empty states - **Empty**: Remove all data, test empty states
For gestures, say what produced the evidence (emulated viewport, synthesized touch, which engine, physical device) and name what stayed untested.
When edge cases are covered, hand off to `$impeccable polish` for the final pass. When edge cases are covered, hand off to `$impeccable polish` for the final pass.
@@ -96,7 +96,7 @@ Build the assigned direction, not a safer interpretation of it. The form supplie
When an approved comp exists, it is a spatial contract, not a mood board: only the user can downgrade its authority, in explicit words. Models systematically believe their HTML, CSS, and SVG recreation of an image succeeded when it did not, so the build runs as a state machine on disk whose gates measure the screen against the comp instead of asking you to remember it. Start it once, and let it tell you what is next: When an approved comp exists, it is a spatial contract, not a mood board: only the user can downgrade its authority, in explicit words. Models systematically believe their HTML, CSS, and SVG recreation of an image succeeded when it did not, so the build runs as a state machine on disk whose gates measure the screen against the comp instead of asking you to remember it. Start it once, and let it tell you what is next:
`.agents/skills/impeccable/scripts/impeccable build-phase start --direction <seed key> --kind <assigned|pick|challenger|canon> --artifact <entry file>` right after the direction choice (this is also the choice ping; the roll's output names the exact command), or `start --comp <approved comp> --artifact <entry file>` when a surface round already locked one. `.agents/skills/impeccable/scripts/impeccable build-phase start --direction <seed key> --kind <assigned|pick|challenger|canon>` right after the direction choice (this is also the choice ping; the roll's output names the exact command), or `start --comp <approved comp>` when a surface round already locked one.
Then, in order, each closed by `.agents/skills/impeccable/scripts/impeccable build-phase advance` (every verb below runs as `.agents/skills/impeccable/scripts/impeccable <verb>`; exit 2 means the gate failed and printed why; fix that and advance again; write nothing for a later phase while an earlier gate is open): Then, in order, each closed by `.agents/skills/impeccable/scripts/impeccable build-phase advance` (every verb below runs as `.agents/skills/impeccable/scripts/impeccable <verb>`; exit 2 means the gate failed and printed why; fix that and advance again; write nothing for a later phase while an earlier gate is open):
@@ -104,9 +104,8 @@ Then, in order, each closed by `.agents/skills/impeccable/scripts/impeccable bui
The comp-led path is a frontier-tier job: it asks the builder to hold a measured layout, place plates at their boxes, and act on numeric readings across a dozen attempts. Smaller or faster models produce a recognisable page and stall under the hero gate; if the model in hand is one of those, say so before the direction round and take the code-led path, or expect the run to end at the hero with its readings unmet. The comp-led path is a frontier-tier job: it asks the builder to hold a measured layout, place plates at their boxes, and act on numeric readings across a dozen attempts. Smaller or faster models produce a recognisable page and stall under the hero gate; if the model in hand is one of those, say so before the direction round and take the code-led path, or expect the run to end at the hero with its readings unmet.
1. **spec.** Measure the comp: `impeccable comp-spec --comp <comp> --grid` writes a coordinate grid over the comp; open it, name every salient region by grid span in a regions file (text and control regions snap to the largest ink mass inside their span, so a headline named B1:E4 measures as the headline and not the column beside it; `snap: false` keeps the span, and an explicit `box` is taken as drawn) (kind `plate` / `image` / `texture` for anything painted: every illustration, photograph, figure, product object, and material texture; `text` / `control` / `chrome` for what code draws; every region carries a `note` saying what the comp shows there, which the plate prompt and the gate messages read), and run `impeccable comp-spec --comp <comp> --regions <file>`. The spec carries each region's box, sampled palette, and medium; `impeccable comp-spec --print` is the build's reference from here on. Type is measured, not guessed: `impeccable font-match --measure <text region>` reads the comp's cap height, width class, and weight off the pixels, and `impeccable font-match --rank <region> --text "..."` takes its candidates from a fingerprint index of the Google Fonts catalog (the nearest faces to the crop's shape) plus any names you pass with `--candidates`, renders them at that cap height with the region's words, and ranks them by fingerprint distance (its `USE` line is the CSS; its proof sheet shows the comp over the top three); with no browser resolvable it records the catalog's nearest face and says the size is estimated, which is still the choice to build on. Do not install a browser to rank, and never write a `chosen` face into the spec by hand: the gate accepts only what font-match wrote. The spec gate refuses to close until the lead text region is measured and ranked. A region note that describes painted material (a diagram, drawing, photograph, texture) under a code kind is refused at the spec: reclassify it as a plate, or reword the note if code really draws it. The script refuses a regions file that leaves comp ink unnamed (callouts, a parts table, a notes block): what is never named can never be missing, so everything the comp shows gets a region. It also refuses a `text` / `control` / `chrome` region larger than a quarter of the comp: that is a column, not an element, and a column scored as one region hides the plates, tables, and notes inside it. Name each element inside it (`container: true` only when it truly is one undivided element). Anything drawn is a plate: an inline SVG past an icon's budget (a diagram, notation, leader lines with arrows, a "quick approximation" of the artwork) is refused at the hero; icon-sized SVG (under 64px, a few paths) is fine, and a chart the page draws from data at runtime is a chart, not an illustration. Callout lines and arrows that annotate a drawing belong to that drawing's plate, with only their labels set as text. A crop of the comp is never a plate (the plates gate refuses a file that is a resample of the comp region: the comp's grain, its neighbours' edges, and its resolution would ship as the artwork); the crop is the reference the plate is generated from. A plate region's box has to hold its whole artwork with a margin: the spec measures the artwork's contact with the box edges and refuses a box that cuts through it (`bleed: true` only when the page really crops it there), because a plate placed with `object-fit: cover` on such a box shows the artwork minus the side the box lost. Anything not in the spec does not exist on the page: no borders, rules, containers, or chrome the comp does not show. Only three concessions exist: fonts (the closest obtainable face), icon glyphs (close enough, exact if the user chose an icon library; this covers the pictogram only, never a control's chrome, so a chevron, an arrow, a dropdown's border and fill, a button's shape are the comp's), and genuine defects in the comp such as spelling errors. 1. **spec.** Measure the comp: `impeccable comp-spec --comp <comp> --grid` writes a coordinate grid over the comp; open it, name every salient region by grid span in a regions file (text and control regions snap to the largest ink mass inside their span, so a headline named B1:E4 measures as the headline and not the column beside it; `snap: false` keeps the span, and an explicit `box` is taken as drawn) (kind `plate` / `image` / `texture` for anything painted: every illustration, photograph, figure, product object, and material texture; `text` / `control` / `chrome` for what code draws; every region carries a `note` saying what the comp shows there, which the plate prompt and the gate messages read), and run `impeccable comp-spec --comp <comp> --regions <file>`. The spec carries each region's box, sampled palette, and medium; `impeccable comp-spec --print` is the build's reference from here on. Type is measured, not guessed: `impeccable font-match --measure <text region>` reads the comp's cap height, width class, and weight off the pixels, and `impeccable font-match --rank <region> --text "..."` takes its candidates from a fingerprint index of the Google Fonts catalog (the nearest faces to the crop's shape) plus any names you pass with `--candidates`, renders them at that cap height with the region's words, and ranks them by fingerprint distance (its `USE` line is the CSS; its proof sheet shows the comp over the top three); with no browser resolvable it records the catalog's nearest face and says the size is estimated, which is still the choice to build on. Do not install a browser to rank, and never write a `chosen` face into the spec by hand: the gate accepts only what font-match wrote. The spec gate refuses to close until the lead text region is measured and ranked. A region note that describes painted material (a diagram, drawing, photograph, texture) under a code kind is refused at the spec: reclassify it as a plate, or reword the note if code really draws it. The script refuses a regions file that leaves comp ink unnamed (callouts, a parts table, a notes block): what is never named can never be missing, so everything the comp shows gets a region. It also refuses a `text` / `control` / `chrome` region larger than a quarter of the comp: that is a column, not an element, and a column scored as one region hides the plates, tables, and notes inside it. Name each element inside it (`container: true` only when it truly is one undivided element). Anything drawn is a plate: an inline SVG past an icon's budget (a diagram, notation, leader lines with arrows, a "quick approximation" of the artwork) is refused at the hero; icon-sized SVG (under 64px, a few paths) is fine, and a chart the page draws from data at runtime is a chart, not an illustration. Callout lines and arrows that annotate a drawing belong to that drawing's plate, with only their labels set as text. A crop of the comp is never a plate (the plates gate refuses a file that is a resample of the comp region: the comp's grain, its neighbours' edges, and its resolution would ship as the artwork); the crop is the reference the plate is generated from. A plate region's box has to hold its whole artwork with a margin: the spec measures the artwork's contact with the box edges and refuses a box that cuts through it (`bleed: true` only when the page really crops it there), because a plate placed with `object-fit: cover` on such a box shows the artwork minus the side the box lost. Anything not in the spec does not exist on the page: no borders, rules, containers, or chrome the comp does not show. Only three concessions exist: fonts (the closest obtainable face), icon glyphs (close enough, exact if the user chose an icon library; this covers the pictogram only, never a control's chrome, so a chevron, an arrow, a dropdown's border and fill, a button's shape are the comp's), and genuine defects in the comp such as spelling errors.
2. **plates.** Every raster region ships as a plate: an illustration, photo, or figure regenerated at asset resolution from its comp crop, UI text removed, at its `plate` path (isolated ink, figures, or objects use native transparent PNG so they sit on the page's own ground; photos and textures stay opaque); a texture (paper, cloth, grain) is a clean patch of the comp region mirror-tiled to size, generated only when no clean patch exists. `impeccable comp-spec --crop <id>` writes the reference; save `impeccable comp-spec --plate-prompt <id> --background transparent` to a prompt file for a cutout, or use `--background opaque` otherwise. Prefer the harness-native image tool with that crop and prompt, then `impeccable embed-prompt <plate> --prompt-file <prompt.txt>`. The API fallback is `impeccable generate-image --ref <crop.png> --prompt-file <prompt.txt> --out <plate.png> --size <WxH> --quality high --background transparent` (use `--background opaque` for full-frame assets); create the output directory first. Verify actual alpha, white foregrounds, fine edges, and clear holes on light and dark grounds; do not chroma-key native output. After the initial assets exist, prepare the isolated code component previews and complete [component-review.md](component-review.md) before further gate-driven repair: the user reviews the whole component kit, including code, before page assembly. This checkpoint keeps the existing gates; it does not require passing them first. After the full page and responsive checks, use the assembled-hero checkpoint before the final response. The plates gate scores the assets against the comp; also inspect placement and scale visually. With parallel subagents, spawn the shipped asset producer (`impeccable-asset-producer`; `impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent") with the spec path and let it produce them all; without subagents, produce them here. A crop of the comp is a reference, never a shipping pixel. The gate checks every plate exists, is at least 1.5x the region's size, and reads as the region. Page code waits for this gate: a page written before its plates exist is a page that draws its material in CSS. A single-file deliverable changes nothing here: the plate is produced the same way and inlined as a data URI. `--force` exists for one case only, the user downgrading the comp's authority in words you quote in `--reason`; the script refuses every other reason. 2. **plates.** Every raster region ships as a plate: an illustration, photo, or figure regenerated at asset resolution from its comp crop, UI text removed, at its `plate` path (isolated ink, figures, or objects use native transparent PNG so they sit on the page's own ground; photos and textures stay opaque); a texture (paper, cloth, grain) is a clean patch of the comp region mirror-tiled to size, generated only when no clean patch exists. `impeccable comp-spec --crop <id>` writes the reference; save `impeccable comp-spec --plate-prompt <id> --background transparent` to a prompt file for a cutout, or use `--background opaque` otherwise. Prefer the harness-native image tool with that crop and prompt, then `impeccable embed-prompt <plate> --prompt-file <prompt.txt>`. The API fallback is `impeccable generate-image --ref <crop.png> --prompt-file <prompt.txt> --out <plate.png> --size <WxH> --quality high --background transparent` (use `--background opaque` for full-frame assets); create the output directory first. Verify actual alpha, white foregrounds, fine edges, and clear holes on light and dark grounds; do not chroma-key native output. The plates gate scores the assets against the comp; also inspect placement and scale visually. With parallel subagents, spawn the shipped asset producer (`impeccable-asset-producer`; `impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent") with the spec path and let it produce them all; without subagents, produce them here. A crop of the comp is a reference, never a shipping pixel. The gate checks every plate exists, is at least 1.5x the region's size, and reads as the region. Page code waits for this gate: a page written before its plates exist is a page that draws its material in CSS. A single-file deliverable changes nothing here: the plate is produced the same way and inlined as a data URI. `--force` exists for one case only, the user downgrading the comp's authority in words you quote in `--reason`; the script refuses every other reason.
3. **hero.** `impeccable build-phase scaffold` first: it writes the measured layout as CSS custom properties (`.impeccable/build/scaffold/layout.css`: `--r-<id>-x/y/w/h` in % of the comp, plus cap height, font-size, family, and weight where measured) and a reference page (`hero-reference.html`) with every region at its box and every plate placed. Bind the numbers to your own semantic structure, an element per region; the reference is a check on positions, never the page, and overlapping boxes are overlapping boxes. Then build only the first viewport, at the comp's own dimensions, the comp's words copied verbatim (the user approved that comp with those words; rewording is a stated decision after the hero passes, never a silent one inside it), every text region sized from its measured cap height and set in its ranked face, plates first: place every plate at its spec box (`object-fit: cover`, an `<img>`, a background image, or an inlined data URI named for it) before any text or control, capture into `.impeccable/review/hero-repro.png`, run `impeccable build-phase record hero` once so you see the plate regions read as match before any text exists, then lay the semantic layer over the plates from the spec's palette and boxes and advance. The gate first refuses while any plate is unreferenced by the source, then runs `impeccable comp-diff`, writes `.impeccable/review/diff/hero/` (side-by-side, heatmap, one paired crop per region, `report.json`), and passes at 72% overall with no hard veto outstanding (a missing region, a contradicted plate or text block, an SVG illustration, a clipped plate, invented ink block at any score); above the bar, the numeric readings become advisories printed with the pass, and the polish pass before responsive is where they get fixed: the gate also reads each text region's cap height, line count, weight, ink colour, and position against the comp, each chrome strip's height off its rule, and the frame for ink where the comp is calm (a kicker, an extra nav item, a divider), and says each miss as a number ("cap height 78px in the build, 103px in the comp"); those numbers are the edit. When it fails, open the region crops it lists, in order, before editing: a region scored `missing` needs its material, `contradicted` needs its structure re-derived from the spec box, `drift` is where size and spacing edits belong; the gate refuses a third attempt that only nudges values on the same region. This is where the run's ambition is won or lost, and a retry here costs minutes where a rebuild verdict at the finish costs the run.
3. **hero.** `impeccable build-phase scaffold` first: it writes the measured layout as CSS custom properties (`.impeccable/build/scaffold/layout.css`: `--r-<id>-x/y/w/h` in % of the comp, plus cap height, font-size, family, and weight where measured) and a reference page (`hero-reference.html`) with every region at its box and every plate placed. Bind the numbers to your own semantic structure, an element per region; the reference is a check on positions, never the page, and overlapping boxes are overlapping boxes. Then build only the first viewport, at the comp's own dimensions, the comp's words copied verbatim (the user approved that comp with those words; rewording is a stated decision after the hero passes, never a silent one inside it), every text region sized from its measured cap height and set in its ranked face, plates first: place every plate at its spec box (`object-fit: cover`, an `<img>`, a background image, or an inlined data URI named for it) before any text or control, capture into `.impeccable/review/hero-repro.png`, run `impeccable build-phase record hero` once so you see the plate regions read as match before any text exists, then lay the semantic layer over the plates from the spec's palette and boxes and advance. The gate first refuses while any plate is unreferenced by the source, then runs `impeccable comp-diff`, writes `.impeccable/review/diff/hero/` (side-by-side, heatmap, one paired crop per region, `report.json`; `raw-report.json` preserves the uninterpreted measurements). The report and crop labels use the gate's verdicts; `gate.reasons` lists the remaining blockers even when a region is called drift. An accepted plate is revalidated if its file, measured region, or comp changes. The gate passes at 72% overall with no hard veto outstanding (a missing region, a contradicted plate or text block, an SVG illustration, a clipped plate, invented ink block at any score); above the bar, the numeric readings become advisories printed with the pass, and the polish pass before responsive is where they get fixed: the gate also reads each text region's cap height, line count, weight, ink colour, and position against the comp, each chrome strip's height off its rule, and the frame for ink where the comp is calm (a kicker, an extra nav item, a divider), and says each miss as a number ("cap height 78px in the build, 103px in the comp"); those numbers are the edit. When it fails, open the region crops it lists, in order, before editing: a region scored `missing` needs its material, `contradicted` needs its structure re-derived from the spec box, `drift` is where size and spacing edits belong; repeated attempts do not clear unresolved blockers. This is where the run's ambition is won or lost, and a retry here costs minutes where a rebuild verdict at the finish costs the run.
4. **sections.** Build the rest of the surface inside the spec's system: the same corner language, line weights, and palette, and nothing the comp never shows. Where the comp does not cover a region, it inherits the recorded system. 4. **sections.** Build the rest of the surface inside the spec's system: the same corner language, line weights, and palette, and nothing the comp never shows. Where the comp does not cover a region, it inherits the recorded system.
5. **motion.** The signature interaction, reveals, and motion, orchestrated once rather than scattered. 5. **motion.** The signature interaction, reveals, and motion, orchestrated once rather than scattered.
6. **responsive.** The other viewports, and the first viewport at common desktop widths (1280 to 1600), not only at the comp's exact size: fluid columns, no fixed-pixel grid that wraps a hundred pixels narrower. Capture `desktop.png` (1440 wide, full page) and `mobile.png` (390 wide) into `.impeccable/review/`; the gate diffs the desktop capture against the comp and refuses a first viewport that only held at the comp's width. A comp'd surface that is mobile-first was comped portrait; the plates were produced for that frame. 6. **responsive.** The other viewports, and the first viewport at common desktop widths (1280 to 1600), not only at the comp's exact size: fluid columns, no fixed-pixel grid that wraps a hundred pixels narrower. Capture `desktop.png` (1440 wide, full page) and `mobile.png` (390 wide) into `.impeccable/review/`; the gate diffs the desktop capture against the comp and refuses a first viewport that only held at the comp's width. A comp'd surface that is mobile-first was comped portrait; the plates were produced for that frame.
@@ -144,5 +143,3 @@ A rebuild and a fix round share one asset rule: a raster either round creates or
Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice.
After the last correction, spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, artifact path, direction contract, PRODUCT.md, [document.md](document.md), and write boundary. Without subagents, load [degraded/documenter.md](degraded/documenter.md) and [document.md](document.md) before writing. Verify the outcome: new worlds and approved system changes require token-bearing DESIGN.md **and** `.impeccable/design.json`, not prose alone. Ordinary extensions compare the finished build against the incumbent system, preserve its files, and report the evidence checked; report pre-existing drift without repairing it unasked. Recheck after later edits. Finish only when review and documentation are complete. After the last correction, spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, artifact path, direction contract, PRODUCT.md, [document.md](document.md), and write boundary. Without subagents, load [degraded/documenter.md](degraded/documenter.md) and [document.md](document.md) before writing. Verify the outcome: new worlds and approved system changes require token-bearing DESIGN.md **and** `.impeccable/design.json`, not prose alone. Ordinary extensions compare the finished build against the incumbent system, preserve its files, and report the evidence checked; report pre-existing drift without repairing it unasked. Recheck after later edits. Finish only when review and documentation are complete.
On a comp-led build, record the final review disposition with `.agents/skills/impeccable/scripts/impeccable build-phase finish --disposition <ship|fix|rebuild|recapture>` before the final response. A refused `ship` is an unfinished build; report the outstanding phase with the verdict.
@@ -16,7 +16,7 @@ Reason over the signals; there is no score to obey:
- `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `$impeccable critique <surface>` is a strong default. - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `$impeccable critique <surface>` is a strong default.
- `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog and closes it when stale or cleared). - `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog and closes it when stale or cleared).
- `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them.
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. **`live` and the bundled `impeccable detect` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with either; the browser overlay and the HTML rule engine don't apply to native app code. - `devServer.running` true → `live` is available for in-browser iteration, and `generate` for one-shot variant runs on a named element; if false, don't lead with either. **`live`, `generate`, and the bundled `impeccable detect` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with any of them; the browser overlay and the HTML rule engine don't apply to native app code.
- Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`. - Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`.
**If `scan.targets` is non-empty and `setup.platform` is not `ios`/`android`/`adaptive`, run `.agents/skills/impeccable/scripts/impeccable detect --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx; it reads HTML/CSS, so skip it for native projects). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it. **If `scan.targets` is non-empty and `setup.platform` is not `ios`/`android`/`adaptive`, run `.agents/skills/impeccable/scripts/impeccable detect --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx; it reads HTML/CSS, so skip it for native projects). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
+1 -1
View File
@@ -1 +1 @@
0.1.6 0.1.5
@@ -19,6 +19,10 @@
"description": "Interactive live variant mode. Select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via HMR. Requires a running dev server. Use when you want to visually experiment with design alternatives in real time.", "description": "Interactive live variant mode. Select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via HMR. Requires a running dev server. Use when you want to visually experiment with design alternatives in real time.",
"argumentHint": "" "argumentHint": ""
}, },
"generate": {
"description": "Agent-driven live variant generation. Boots live mode, finds the named element on the open page, scrolls the browser to it, and delivers N variants in the requested direction for the user to cycle and accept. Use for requests that name an element and a direction, like 'generate 3 bold variants of the pricing cards', skipping manual element picking.",
"argumentHint": "[count] [direction] variants of [element]"
},
"adapt": { "adapt": {
"description": "Adapt designs to work across different screen sizes, devices, contexts, or platforms. Implements breakpoints, fluid layouts, and touch targets. Use when the user mentions responsive design, mobile layouts, breakpoints, viewport adaptation, or cross-device compatibility.", "description": "Adapt designs to work across different screen sizes, devices, contexts, or platforms. Implements breakpoints, fluid layouts, and touch targets. Use when the user mentions responsive design, mobile layouts, breakpoints, viewport adaptation, or cross-device compatibility.",
"argumentHint": "[target] [context (mobile, tablet, print...)]" "argumentHint": "[target] [context (mobile, tablet, print...)]"
@@ -165,6 +165,14 @@
} }
let parameterGenerationState = 'idle'; let parameterGenerationState = 'idle';
let parameterReadyAnnouncedSession = null; let parameterReadyAnnouncedSession = null;
// 'agent' when the generate verb fired this session's Go (the generate
// lane declares no knobs, so its bar never shows a pending Tune chip);
// null for every Go a user presses.
let sessionOrigin = null;
// The generate lane picks for the agent and never edits copy in the
// browser, so its selection carries no edit-copy badge (set on the
// agent-target pick, cleared with the session; a user's pick never sets it).
let editBadgeSuppressed = false;
let svelteComponentSession = null; let svelteComponentSession = null;
let svelteRuntimePromise = null; let svelteRuntimePromise = null;
let pendingSvelteComponentRetryObserver = null; let pendingSvelteComponentRetryObserver = null;
@@ -983,9 +991,20 @@
} }
} catch { /* cross-origin */ } } catch { /* cross-origin */ }
} }
// The selector a mechanical bake would anchor lasting rules on, and how
// many elements it matches right now: the bake refuses anything but one,
// since its rules would restyle every match, not just this element.
const cssIdent = (s) => /^[A-Za-z_-][\w-]*$/.test(s);
const anchorClasses = [...el.classList].filter(cssIdent);
const anchor = el.id && cssIdent(el.id)
? '#' + el.id
: (anchorClasses.length ? el.tagName.toLowerCase() + '.' + anchorClasses.join('.') : null);
let anchorMatches = null;
if (anchor) { try { anchorMatches = document.querySelectorAll(anchor).length; } catch { anchorMatches = null; } }
return { return {
tagName: el.tagName.toLowerCase(), id: el.id || null, tagName: el.tagName.toLowerCase(), id: el.id || null,
classes: [...el.classList], classes: [...el.classList],
anchor, anchorMatches,
textContent: (el.textContent || '').slice(0, 500), textContent: (el.textContent || '').slice(0, 500),
outerHTML: sanitizedContextOuterHTML(el, 10000), outerHTML: sanitizedContextOuterHTML(el, 10000),
computedStyles: { computedStyles: {
@@ -2037,6 +2056,7 @@
function setLiveState(next) { function setLiveState(next) {
state = next; state = next;
window.__IMPECCABLE_LIVE_STATE__ = next; window.__IMPECCABLE_LIVE_STATE__ = next;
retryDeclinedAgentTargets();
syncPageInteractionCursor(); syncPageInteractionCursor();
// Whether a queued steer is still behind a generation is a function of this // Whether a queued steer is still behind a generation is a function of this
// state, so the hint has to move with it, not only with the 5s poll. // state, so the hint has to move with it, not only with the 5s poll.
@@ -4014,6 +4034,7 @@
function hidePendingApplyDock() { function hidePendingApplyDock() {
pendingApplyInFlight = false; pendingApplyInFlight = false;
retryDeclinedAgentTargets();
clearStoredManualApplyState(); clearStoredManualApplyState();
if (pendingIntroAnimation) { pendingIntroAnimation.cancel(); pendingIntroAnimation = null; } if (pendingIntroAnimation) { pendingIntroAnimation.cancel(); pendingIntroAnimation = null; }
if (pendingDockEl) pendingDockEl.style.display = 'none'; if (pendingDockEl) pendingDockEl.style.display = 'none';
@@ -4047,6 +4068,7 @@
function setPendingApplyLoading(loading, count) { function setPendingApplyLoading(loading, count) {
if (!pendingPillEl || !pendingPillLabelEl || !pendingPillCountEl || !pendingTrashBtn) return; if (!pendingPillEl || !pendingPillLabelEl || !pendingPillCountEl || !pendingTrashBtn) return;
pendingApplyInFlight = loading === true; pendingApplyInFlight = loading === true;
if (!pendingApplyInFlight) retryDeclinedAgentTargets();
const currentCount = count || parseInt(pendingPillEl.dataset.count || '0', 10) || 0; const currentCount = count || parseInt(pendingPillEl.dataset.count || '0', 10) || 0;
if (pendingApplyInFlight) storeManualApplyState(currentCount); if (pendingApplyInFlight) storeManualApplyState(currentCount);
else clearStoredManualApplyState(); else clearStoredManualApplyState();
@@ -4688,6 +4710,7 @@
} }
function renderEditBadge(mode) { function renderEditBadge(mode) {
if (editBadgeSuppressed || sessionOrigin === 'agent') mode = 'hidden';
if (mode === 'hidden' || !editBadgeEl) { if (mode === 'hidden' || !editBadgeEl) {
hideConfigureBarTooltip(); hideConfigureBarTooltip();
if (editBadgeEl) editBadgeEl.style.display = 'none'; if (editBadgeEl) editBadgeEl.style.display = 'none';
@@ -6181,6 +6204,8 @@
resetSessionFileMeta(); resetSessionFileMeta();
currentSessionId = null; currentSessionId = null;
parameterGenerationState = 'idle'; parameterGenerationState = 'idle';
sessionOrigin = null;
editBadgeSuppressed = false;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
expectedVariants = 0; expectedVariants = 0;
arrivedVariants = 0; arrivedVariants = 0;
@@ -7112,6 +7137,398 @@
} }
// //
// ------------------------------------------------------------------
// Agent-initiated targeting (the `generate` command). The agent names an
// element by CSS selector over POST /agent-target; the server pushes an
// `agent_target` SSE message here. The overlay resolves the selector,
// scrolls the element into view, enters the same picked state a user
// click produces, and fires the normal Go pipeline, so everything
// downstream (generate event, variants, cycling, accept) is unchanged.
// The verdict goes back through POST /agent-target-result, which resolves
// the agent's held-open CLI call.
function postAgentTargetResult(targetId, result) {
fetch('http://localhost:' + PORT + '/agent-target-result?token=' + TOKEN, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: TOKEN, targetId, clientId: AGENT_TARGET_CLIENT_ID, ...result }),
}).catch(() => { /* server gone; nothing to report to */ });
}
function describeAgentTargetCandidate(el) {
return {
tag: el.tagName.toLowerCase(),
id: el.id || null,
classes: [...el.classList].filter((c) => !c.startsWith('impeccable-')),
text: (el.textContent || '').trim().slice(0, 80),
};
}
function resolveAgentTargetElement(msg) {
let matched;
try {
matched = [...document.querySelectorAll(msg.selector)];
} catch {
return { error: { ok: false, error: 'invalid_selector', selector: msg.selector } };
}
let candidates = matched.filter((el) => pickable(el));
if (msg.text) {
const needle = String(msg.text).toLowerCase();
candidates = candidates.filter((el) => (el.textContent || '').toLowerCase().includes(needle));
}
if (candidates.length === 0) {
return {
error: {
ok: false,
error: 'no_match',
selector: msg.selector,
matchCount: 0,
// How many nodes the raw selector hit before the pickable/text
// filters: distinguishes a wrong selector from an unpickable match.
rawMatchCount: matched.length,
},
};
}
if (Number.isInteger(msg.index)) {
const el = candidates[msg.index - 1];
if (!el) {
return { error: { ok: false, error: 'index_out_of_range', selector: msg.selector, matchCount: candidates.length } };
}
return { el, matchCount: candidates.length };
}
if (candidates.length > 1) {
return {
error: {
ok: false,
error: 'ambiguous',
selector: msg.selector,
matchCount: candidates.length,
candidates: candidates.slice(0, 8).map(describeAgentTargetCandidate),
},
};
}
return { el: candidates[0], matchCount: 1 };
}
function scrollAgentTargetIntoView(el, done) {
const rect = el.getBoundingClientRect();
if (rect.top >= 0 && rect.bottom <= window.innerHeight) { done(); return; }
let settled = false;
let fallback = null;
const finish = () => {
if (settled) return;
settled = true;
removeEventListener('scrollend', finish, true);
if (fallback) clearTimeout(fallback);
done();
};
// scrollend where supported; a timer covers engines without it and the
// no-movement case (element already at its final resting position).
addEventListener('scrollend', finish, true);
fallback = setTimeout(finish, 1200);
el.scrollIntoView({ block: 'center', behavior: 'smooth' });
}
// One id per page load: the server keys claims and roll-call reports on
// it, and only the tab that holds the lease can renew it.
const AGENT_TARGET_CLIENT_ID = id8();
// The agent target an agent-initiated Go is serving: set by
// actOnAgentTarget around its handleGo call, read once by handleGo.
let agentTargetForGo = null;
// The helper's word on its global bar. The generate lane asks the helper
// to keep it out of the way (`impeccable live --no-live-bar`, or an agent
// target carrying hideLiveBar), and the helper tells every connected tab
// at once (`live_bar`) and every later connection on `connected`, so the
// bar stays hidden in every tab, through reloads, the accept, and the
// bake, until the helper stops and takes the overlay with it. The variant
// controls still show.
let liveBarHiddenByHelper = false;
function applyLiveBarPreference(hidden) {
liveBarHiddenByHelper = hidden === true;
setLiveBarHidden(liveBarHiddenByHelper);
}
// A plain live session must never notice this code: hiding remembers the
// bar's own display value and restoring puts exactly that back, and a
// restore on a bar that is not hidden is a no-op, so the `connected`
// frame every session receives changes nothing unless the lane asked.
function setLiveBarHidden(hidden) {
if (!globalBarEl) return;
if (hidden) {
if (globalBarEl.style.display !== 'none') {
globalBarEl.dataset.liveBarDisplay = globalBarEl.style.display || 'flex';
globalBarEl.style.display = 'none';
}
return;
}
if (globalBarEl.style.display === 'none') {
globalBarEl.style.display = globalBarEl.dataset.liveBarDisplay || 'flex';
}
}
function claimAgentTarget(targetId, report) {
return fetch('http://localhost:' + PORT + '/agent-target-claim?token=' + TOKEN, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: TOKEN, targetId, clientId: AGENT_TARGET_CLIENT_ID, ...report }),
}).then((res) => res.json())
.then((j) => ({ granted: !!j && j.granted === true, pending: !!j && j.pending === true }))
.catch(() => ({ granted: false, pending: false }));
}
// `exceptTargetId` is the target this call is about: a tab acting on it
// is not busy for itself, but it is busy for every other target, or two
// held requests could both be claimed here and the second Go would
// overwrite the session the first one minted.
function agentTargetBusyReason(exceptTargetId) {
if (pendingApplyInFlight) return 'manual_apply_in_flight';
if (state !== 'IDLE' && state !== 'PICKING' && state !== 'CONFIGURING') return 'session_active';
for (const [targetId, status] of agentTargetsSeen) {
if (status === 'acting' && targetId !== exceptTargetId) return 'agent_target_in_flight';
}
return null;
}
// Targets this tab declined as busy. A busy report is only this tab's word
// at that moment: the moment it is free again (setLiveState), it claims
// each of these as eligible, and the server drops the stale report, so a
// busy verdict is never built on a tab that has since gone idle. The
// server denies claims for resolved targets, so retries are harmless.
const busyDeclinedTargets = new Map();
function declineAgentTargetBusy(msg, busy) {
busyDeclinedTargets.set(msg.targetId, msg);
noteAgentTarget(msg.targetId, 'declined');
claimAgentTarget(msg.targetId, { eligible: false, state, reason: busy });
}
// A torn-down overlay, or one whose helper connection is gone, cannot
// serve a target and must not even claim one: it would hold the lease for
// a request it will never act on.
function agentTargetOverlayGone() {
return !evtSource;
}
// A denied claimant retries at this cadence, a little over the lease, so
// the first retry after a dead holder's lease lapses is granted.
const AGENT_TARGET_RESCUE_RETRY_MS = 3500;
// Claim the lease and act as the holder. A denied claim means another tab
// holds the lease. That holder can die before posting its result (reload,
// crash, even after renewing), and its lease lapses after ~3s, so this tab
// keeps retrying for as long as the server still holds the request: the
// answer's `pending` is the server's word that the request is alive, and
// it turns false the moment the request resolved or timed out, so no tab
// retries a request nobody awaits. A tab that turned busy meanwhile joins
// the roll call instead of taking a lease it cannot use. The first claim
// and the busy-to-idle re-claim share this.
function claimAndActOnAgentTarget(msg) {
if (agentTargetOverlayGone()) return;
const busy = agentTargetBusyReason(msg.targetId);
if (busy) { declineAgentTargetBusy(msg, busy); return; }
if (declineAgentTargetUnresolvable(msg)) return;
claimAgentTarget(msg.targetId, { eligible: true }).then((claim) => {
if (claim.granted) { noteAgentTarget(msg.targetId, 'acting'); actOnAgentTarget(msg); return; }
noteAgentTarget(msg.targetId, 'denied');
if (!claim.pending) return;
setTimeout(() => claimAndActOnAgentTarget(msg), AGENT_TARGET_RESCUE_RETRY_MS);
});
}
function retryDeclinedAgentTargets() {
if (busyDeclinedTargets.size === 0 || agentTargetBusyReason()) return;
for (const [targetId, msg] of busyDeclinedTargets) {
busyDeclinedTargets.delete(targetId);
claimAndActOnAgentTarget(msg);
}
}
// This page's participation in each target it heard: 'acting' once a
// claim was granted, 'done' once it replied (or stood down from a lapsed
// lease), else the word it last gave. The server replays pending targets
// to every connection that opens. After a reconnect that overlapped the
// old connection the server still holds this page's word; after one that
// did not, it dropped the word on the close, so a replayed target is
// handled again: a busy or unresolvable page re-declines (idempotent), an
// idle page claims.
const agentTargetsSeen = new Map();
function noteAgentTarget(targetId, status) {
agentTargetsSeen.set(targetId, status);
if (agentTargetsSeen.size > 100) agentTargetsSeen.delete(agentTargetsSeen.keys().next().value);
}
// A target this page took a lease on is off-limits for a replay: while
// acting (a second claim or Go), and once done, because its result may
// still be on the wire and this tab is GENERATING by then, so handling
// the replay would decline busy, hand the lease back mid-resolution, and
// let another tab fire a second Go.
function agentTargetTaken(targetId) {
const status = agentTargetsSeen.get(targetId);
return status === 'acting' || status === 'done';
}
// Only a page that can resolve the target claims it. A tab whose page
// lacks the element declines with its resolution verdict instead, so a
// first-wins claim never lets the wrong page answer for a target that
// another page has. The server prefers a busy report (a tab that could
// serve later) over these, and returns the resolution verdict only when
// no connected page can serve.
//
// An element can be momentarily absent (a route still rendering, an HMR
// commit mid-swap), so a failed resolution is not this page's final word:
// it is re-checked a few times over about two seconds, claiming the
// moment the element mounts, and only the last miss is reported. The
// server's timeout still bounds the whole exchange.
// The page reports the miss at once (so the other overlays' words can
// complete the roll call) and keeps re-checking at this cadence for as
// long as the server says the request is pending: the server holds an
// all-no_match roll call open for a short grace precisely so a late mount
// can still be claimed, drops the stale report on an eligible claim, and
// ends the watch by answering pending:false once the request resolved or
// timed out.
const AGENT_TARGET_RESOLVE_WATCH_MS = 500;
function declineAgentTargetUnresolvable(msg) {
const probe = resolveAgentTargetElement(msg);
if (!probe.error) return false;
reportAgentTargetUnresolvable(msg, probe.error);
return true;
}
function reportAgentTargetUnresolvable(msg, error) {
noteAgentTarget(msg.targetId, 'declined');
claimAgentTarget(msg.targetId, { eligible: false, state, reason: 'no_match', result: error }).then((answer) => {
if (!answer.pending) return;
setTimeout(() => watchAgentTargetResolution(msg, error), AGENT_TARGET_RESOLVE_WATCH_MS);
});
}
function watchAgentTargetResolution(msg, lastError) {
if (agentTargetOverlayGone() || agentTargetTaken(msg.targetId)) return;
const busy = agentTargetBusyReason(msg.targetId);
if (busy) { declineAgentTargetBusy(msg, busy); return; }
const probe = resolveAgentTargetElement(msg);
if (!probe.error) { claimAndActOnAgentTarget(msg); return; }
// Still unresolvable: re-report (idempotent); the answer says whether
// the server is still holding the request open.
reportAgentTargetUnresolvable(msg, probe.error || lastError);
}
function handleAgentTarget(msg) {
if (!msg || typeof msg.targetId !== 'string') return;
if (agentTargetTaken(msg.targetId)) return;
noteAgentTarget(msg.targetId, 'heard');
const busy = agentTargetBusyReason(msg.targetId);
if (busy) {
// Roll call: a busy tab reports itself and never acts. The server
// answers `busy` the moment every connected overlay has reported, so
// an idle tab elsewhere is never raced by a timer.
declineAgentTargetBusy(msg, busy);
return;
}
if (declineAgentTargetUnresolvable(msg)) return;
// Eligible tabs race for the server's lease and only the holder acts. A
// hidden tab yields a short head start so a visible one wins when both
// exist, and still serves the request on its own: the user finds the
// selection waiting when they return to it.
setTimeout(() => claimAndActOnAgentTarget(msg), document.hidden ? 150 : 0);
}
function actOnAgentTarget(msg) {
if (agentTargetOverlayGone()) return;
// Every exit ends this tab's acting state, so a later target is not
// refused for a Go that already happened or never will.
const reply = (result) => { noteAgentTarget(msg.targetId, 'done'); postAgentTargetResult(msg.targetId, result); };
const busy = agentTargetBusyReason(msg.targetId);
if (busy) {
// Turned busy between claim and act: report it, which also hands the
// lease back so the roll call can complete or a rescuer can claim.
declineAgentTargetBusy(msg, busy);
return;
}
const resolved = resolveAgentTargetElement(msg);
if (resolved.error) {
// The element went away between claim and act. A result would end the
// request for every tab; a decline hands the lease back so another
// page or a remount can still serve it.
reportAgentTargetUnresolvable(msg, resolved.error);
return;
}
const el = resolved.el;
if (msg.dryRun) {
reply({
ok: true,
dryRun: true,
matchCount: resolved.matchCount,
element: describeAgentTargetCandidate(el),
});
return;
}
scrollAgentTargetIntoView(el, () => {
// Torn down during the scroll settle: do not renew. The lease lapses
// for a rescuer instead of Go minting a session on a dismantled
// overlay.
if (agentTargetOverlayGone()) return;
// Renew the lease right before the irreversible part: a tab whose
// lease lapsed while it scrolled (a rescuer took over) stops here, so
// one request never gets two Go presses.
claimAgentTarget(msg.targetId, { eligible: true }).then((renewal) => {
if (!renewal.granted) { noteAgentTarget(msg.targetId, 'done'); return; }
// An insert placement left mid-configure gives way, exactly as a
// click outside it does in handleClick.
if (state === 'CONFIGURING' && configureKind === 'insert') cancelInsertConfigure();
// Mirror of the user-click pick entry in handleClick, minus the
// pick-mode gate (the agent's intent replaces the toggle); the entry
// goes through beginNewLiveConfiguration like every other pick so
// deferred recovery sees a fresh interaction revision.
selectedElement = el;
beginNewLiveConfiguration();
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
showBar('configure');
editBadgeSuppressed = true;
renderEditBadge('hidden');
startScrollTracking();
maybePrefetchPage();
maybeWarnConditionalAncestor(selectedElement);
// Preset what the agent asked for, then fire the same Go a user press
// fires. handleGo reads exactly these inputs.
selectedAction = msg.action;
selectedCount = msg.count;
// updateBarContent rebuilds the configure row and replaces the input
// element, so the prompt must be written into the input it creates,
// never before (the action-chip click handler does the same dance).
updateBarContent('configure');
const input = uiGetById(PREFIX + '-input');
if (input) input.value = msg.prompt || '';
// The target rides on the generate event too: the helper resolves
// the request from whichever lands first, so a page that dies
// between Go and its result cannot leave the request pending for a
// second Go elsewhere.
const candidate = describeAgentTargetCandidate(el);
agentTargetForGo = { targetId: msg.targetId, matchCount: resolved.matchCount, action: msg.action, count: msg.count, element: candidate };
handleGo();
agentTargetForGo = null;
if (state === 'GENERATING' && currentSessionId) {
reply({
ok: true,
matchCount: resolved.matchCount,
sessionId: currentSessionId,
action: msg.action,
count: msg.count,
element: candidate,
});
} else {
reply({ ok: false, error: 'go_failed', state });
}
});
});
}
// SSE (server→browser) + fetch POST (browser→server) // SSE (server→browser) + fetch POST (browser→server)
// Zero-dependency replacement for WebSocket. // Zero-dependency replacement for WebSocket.
// //
@@ -7121,7 +7538,7 @@
const SSE_MAX_RETRIES = 20; // generous: heartbeats keep the connection alive, so retries mean real trouble const SSE_MAX_RETRIES = 20; // generous: heartbeats keep the connection alive, so retries mean real trouble
function connectSSE() { function connectSSE() {
evtSource = new EventSource('http://localhost:' + PORT + '/events?token=' + TOKEN); evtSource = new EventSource('http://localhost:' + PORT + '/events?token=' + TOKEN + '&clientId=' + AGENT_TARGET_CLIENT_ID);
evtSource.onopen = () => { evtSource.onopen = () => {
sseRetries = 0; // reset on successful (re)connect sseRetries = 0; // reset on successful (re)connect
@@ -7132,8 +7549,11 @@
let msg; try { msg = JSON.parse(e.data); } catch { return; } let msg; try { msg = JSON.parse(e.data); } catch { return; }
switch (msg.type) { switch (msg.type) {
case 'connected': case 'connected':
applyLiveBarPreference(msg.hideLiveBar === true);
hasProjectContext = !!msg.hasProjectContext; hasProjectContext = !!msg.hasProjectContext;
if (!hasProjectContext) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000); // The generate lane runs without PRODUCT.md by design and never
// sends the user to init, so its quiet chrome skips this notice.
if (!hasProjectContext && !liveBarHiddenByHelper) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000);
console.log('[impeccable] Live mode connected.'); console.log('[impeccable] Live mode connected.');
syncAgentPollingUi(!!msg.agentPolling); syncAgentPollingUi(!!msg.agentPolling);
startAgentStatusPoll(); startAgentStatusPoll();
@@ -7143,9 +7563,15 @@
syncPageInteractionCursor(); syncPageInteractionCursor();
syncPageChatFocus('sse-connected'); syncPageChatFocus('sse-connected');
break; break;
case 'live_bar':
applyLiveBarPreference(msg.hidden === true);
break;
case 'agent_polling': case 'agent_polling':
syncAgentPollingUi(!!msg.connected); syncAgentPollingUi(!!msg.connected);
break; break;
case 'agent_target':
handleAgentTarget(msg);
break;
case 'agent_phase': case 'agent_phase':
if (msg.id === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) { if (msg.id === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) {
// Advance the visible phase monotonically. A behind/resumed // Advance the visible phase monotonically. A behind/resumed
@@ -7208,6 +7634,11 @@
disableInlineEdit(); disableInlineEdit();
refreshParamsPanel(); refreshParamsPanel();
} }
// The done reply is the agent's last word on this generation:
// with every variant mounted and no knobs declared, the Tune
// chip must stop spinning. A reload between the mount and this
// reply restored the pending state from the cache.
completeParameterGenerationIfReady();
break; break;
} }
// Source fallback when HMR did not land variants in this tab. // Source fallback when HMR did not land variants in this tab.
@@ -7371,6 +7802,15 @@
}).then(async res => { }).then(async res => {
if (res.ok) return res; if (res.ok) return res;
const body = await res.json().catch(() => ({})); const body = await res.json().catch(() => ({}));
// The helper refused to open a session for an agent target it has
// already answered (another page served it after this page's lease
// lapsed mid-capture, or the request timed out): drop the local
// session and hand the surface back.
if (body.error === 'agent_target_already_served' && msg.type === 'generate'
&& msg.id && msg.id === currentSessionId) {
abandonSupersededGo(msg.id);
return null;
}
// The server refused to journal progress for a session it has never // The server refused to journal progress for a session it has never
// seen: this browser is carrying state from another project or a // seen: this browser is carrying state from another project or a
// wiped store (two apps sharing a localhost port). Continuing to // wiped store (two apps sharing a localhost port). Continuing to
@@ -7392,6 +7832,14 @@
return sessionCreationGate.then(doSend); return sessionCreationGate.then(doSend);
} }
function abandonSupersededGo(sessionId) {
if (sessionId !== currentSessionId) return;
console.warn('[impeccable] The helper already answered this agent target; clearing session ' + sessionId + '.');
markSessionHandled();
cleanup({ instantChrome: true });
showToast('The helper already answered this request, so this session was cleared. Pick an element to start fresh.', 6000);
}
let abandonedForeignSessionId = null; let abandonedForeignSessionId = null;
function abandonForeignSession(sessionId) { function abandonForeignSession(sessionId) {
if (abandonedForeignSessionId === sessionId || sessionId !== currentSessionId) return; if (abandonedForeignSessionId === sessionId || sessionId !== currentSessionId) return;
@@ -7796,6 +8244,7 @@
visibleVariant = 0; visibleVariant = 0;
generationPhase = 'queued'; generationPhase = 'queued';
parameterGenerationState = 'pending'; parameterGenerationState = 'pending';
sessionOrigin = agentTargetForGo ? 'agent' : null;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
resetSessionFileMeta(); resetSessionFileMeta();
@@ -7821,6 +8270,24 @@
}; };
if (snapshot.comments.length > 0) basePayload.comments = snapshot.comments; if (snapshot.comments.length > 0) basePayload.comments = snapshot.comments;
if (snapshot.strokes.length > 0) basePayload.strokes = snapshot.strokes; if (snapshot.strokes.length > 0) basePayload.strokes = snapshot.strokes;
if (agentTargetForGo) {
// An agent-initiated Go names the target it serves (see
// actOnAgentTarget): the helper resolves that request from this event
// as well as from the overlay's own result post.
basePayload.agentTarget = {
targetId: agentTargetForGo.targetId,
clientId: AGENT_TARGET_CLIENT_ID,
result: {
ok: true,
matchCount: agentTargetForGo.matchCount,
sessionId: currentSessionId,
action: agentTargetForGo.action,
count: agentTargetForGo.count,
element: agentTargetForGo.element,
},
};
agentTargetForGo = null;
}
// Hide the interactive overlay so it doesn't linger during generation. // Hide the interactive overlay so it doesn't linger during generation.
hideAnnotOverlay(); hideAnnotOverlay();
@@ -7881,6 +8348,7 @@
visibleVariant = 0; visibleVariant = 0;
generationPhase = 'queued'; generationPhase = 'queued';
parameterGenerationState = 'pending'; parameterGenerationState = 'pending';
sessionOrigin = agentTargetForGo ? 'agent' : null;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
resetSessionFileMeta(); resetSessionFileMeta();
selectedElement = placeholderElement; selectedElement = placeholderElement;
@@ -8927,6 +9395,8 @@ void main() {
pagePickSkipClick = false; pagePickSkipClick = false;
currentSessionId = null; currentSessionId = null;
parameterGenerationState = 'idle'; parameterGenerationState = 'idle';
sessionOrigin = null;
editBadgeSuppressed = false;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
selectedAction = 'impeccable'; selectedAction = 'impeccable';
pendingAcceptedSession = null; pendingAcceptedSession = null;
@@ -9018,6 +9488,7 @@ void main() {
paramsCurrentValues = { ...saved.paramValues }; paramsCurrentValues = { ...saved.paramValues };
} }
if (saved.parameterState) parameterGenerationState = saved.parameterState; if (saved.parameterState) parameterGenerationState = saved.parameterState;
sessionOrigin = saved.origin === 'agent' ? 'agent' : null;
if (saved.generationPhase) generationPhase = saved.generationPhase; if (saved.generationPhase) generationPhase = saved.generationPhase;
} }
@@ -9105,7 +9576,12 @@ void main() {
} }
function restoreSessionWithoutWrapper(reason, activeSessions) { function restoreSessionWithoutWrapper(reason, activeSessions) {
const cached = loadSession(); // The session cache is per origin, so a tab on another page of the same
// app sees this page's session too. Only the page that saved it may
// resume it: the server-adoption branch below already applies the same
// check, and a tab on another page has nothing to render for it.
const cachedRaw = loadSession();
const cached = cachedRaw?.id && !pageMatchesCurrent(cachedRaw.pageUrl) ? null : cachedRaw;
// localStorage is a cache, not a gate. A cleared tab, a second browser // localStorage is a cache, not a gate. A cleared tab, a second browser
// profile, or a teardown that dropped local state all leave the durable // profile, or a teardown that dropped local state all leave the durable
// server session as the only record of work in progress; adopt it instead // server session as the only record of work in progress; adopt it instead
@@ -9218,6 +9694,7 @@ void main() {
pageUrl: location.pathname, pageUrl: location.pathname,
paramValues: { ...paramsCurrentValues }, paramValues: { ...paramsCurrentValues },
parameterState: parameterGenerationState, parameterState: parameterGenerationState,
origin: sessionOrigin || undefined,
insertPlaceholder: insertPlaceholderSnapshot || undefined, insertPlaceholder: insertPlaceholderSnapshot || undefined,
pickedAnchor: pickedAnchorSnapshot || undefined, pickedAnchor: pickedAnchorSnapshot || undefined,
pickedAnchorViewportTop: Number.isFinite(pickedAnchorViewportTop) ? pickedAnchorViewportTop : undefined, pickedAnchorViewportTop: Number.isFinite(pickedAnchorViewportTop) ? pickedAnchorViewportTop : undefined,
@@ -9343,6 +9820,8 @@ void main() {
pagePickSkipClick = false; pagePickSkipClick = false;
currentSessionId = null; currentSessionId = null;
parameterGenerationState = 'idle'; parameterGenerationState = 'idle';
sessionOrigin = null;
editBadgeSuppressed = false;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
selectedAction = 'impeccable'; selectedAction = 'impeccable';
renderEditBadge('hidden'); renderEditBadge('hidden');
@@ -9601,6 +10080,14 @@ void main() {
const resumedState = arrivedVariants > 0 ? 'CYCLING' : 'GENERATING'; const resumedState = arrivedVariants > 0 ? 'CYCLING' : 'GENERATING';
// A reload between the variants mounting and the agent's done reply
// restores a pending Tune state from the cache; the helper knows whether
// that generation already finished.
if (arrivedVariants >= expectedVariants && expectedVariants > 0
&& (parameterGenerationState === 'pending' || parameterGenerationState === 'loading')) {
settleParameterStateFromHelper(sessionId);
}
// Find the visible variant's content element for highlight positioning. // Find the visible variant's content element for highlight positioning.
const isInsert = wrapper.dataset.impeccableMode === 'insert'; const isInsert = wrapper.dataset.impeccableMode === 'insert';
const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null; const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null;
@@ -11065,6 +11552,21 @@ void main() {
} }
} }
// After a resume the cache may say the Tune knobs are still coming while
// the agent already replied done before the reload. The helper's session
// record settles it; otherwise the done reply on SSE does.
function settleParameterStateFromHelper(sessionId) {
fetch('http://localhost:' + PORT + '/status?token=' + TOKEN, { cache: 'no-store' })
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
if (!data || sessionId !== currentSessionId) return;
const session = (data.activeSessions || []).find((s) => s && s.id === sessionId);
if (!session) return;
if (session.generationCompletedAt || session.generationPhase === 'completed') completeParameterGenerationIfReady();
})
.catch(() => { /* the done reply on SSE settles it otherwise */ });
}
function fetchAgentPollingStatus() { function fetchAgentPollingStatus() {
fetch('http://localhost:' + PORT + '/status?token=' + TOKEN, { cache: 'no-store' }) fetch('http://localhost:' + PORT + '/status?token=' + TOKEN, { cache: 'no-store' })
.then((res) => (res.ok ? res.json() : null)) .then((res) => (res.ok ? res.json() : null))
@@ -11104,11 +11606,15 @@ void main() {
uiAppendStyle(s); uiAppendStyle(s);
} }
// The generate lane's helper says so in the served script itself, so a
// lane session never draws the bar at all; every other session mounts
// it exactly as before.
const barHiddenFromStart = window.__IMPECCABLE_LIVE_BAR_HIDDEN__ === true;
globalBarEl = el('div', { globalBarEl = el('div', {
position: 'fixed', bottom: '14px', left: '50%', position: 'fixed', bottom: '14px', left: '50%',
transform: 'translateX(-50%) translateY(20px)', transform: 'translateX(-50%) translateY(20px)',
zIndex: Z.bar + 5, zIndex: Z.bar + 5,
display: 'flex', alignItems: 'stretch', display: barHiddenFromStart ? 'none' : 'flex', alignItems: 'stretch',
gap: '0', gap: '0',
width: 'max-content', width: 'max-content',
background: P.surface, background: P.surface,
@@ -11124,6 +11630,10 @@ void main() {
}); });
globalBarEl.id = PREFIX + '-global-bar'; globalBarEl.id = PREFIX + '-global-bar';
globalBarEl.dataset.theme = theme; globalBarEl.dataset.theme = theme;
if (barHiddenFromStart) {
liveBarHiddenByHelper = true;
globalBarEl.dataset.liveBarDisplay = 'flex';
}
// Brand mark - kinpaku Impeccable icon (site header / favicon paths). // Brand mark - kinpaku Impeccable icon (site header / favicon paths).
const brand = el('span', { const brand = el('span', {
@@ -11519,6 +12029,9 @@ void main() {
// Listen for detection results AND ready signal // Listen for detection results AND ready signal
window.addEventListener('message', onDetectMessage); window.addEventListener('message', onDetectMessage);
updateGlobalBarState(); updateGlobalBarState();
// The helper may already have said the bar stays hidden (a connect
// that raced the bar build, or a reload mid-lane): re-apply it here.
if (liveBarHiddenByHelper) setLiveBarHidden(true);
} }
function updateGlobalBarState() { function updateGlobalBarState() {
@@ -11715,6 +12228,13 @@ void main() {
/** Full teardown: remove all UI, disconnect SSE, clean up. */ /** Full teardown: remove all UI, disconnect SSE, clean up. */
function teardown() { function teardown() {
// Declined targets die with the overlay: the IDLE transition below must
// not re-claim a lease this page can no longer act on. So does the
// target ledger: an 'acting' entry from a Go that never happened must
// not refuse every target the next connection hears.
busyDeclinedTargets.clear();
agentTargetsSeen.clear();
liveBarHiddenByHelper = false;
stopAgentStatusPoll(); stopAgentStatusPoll();
hideAgentPollTooltip(); hideAgentPollTooltip();
if (agentPollTooltipEl) { if (agentPollTooltipEl) {
-1
View File
@@ -1 +0,0 @@
1.3.13
+3 -3
View File
@@ -2,7 +2,7 @@
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json", "$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
"name": "impeccable", "name": "impeccable",
"metadata": { "metadata": {
"description": "Design fluency for AI harnesses. 1 skill, 23 commands, and curated anti-patterns for impeccable frontend design." "description": "Design fluency for AI harnesses. 1 skill, 24 commands, and curated anti-patterns for impeccable frontend design."
}, },
"owner": { "owner": {
"name": "Paul Bakaus", "name": "Paul Bakaus",
@@ -11,8 +11,8 @@
"plugins": [ "plugins": [
{ {
"name": "impeccable", "name": "impeccable",
"description": "Design fluency for frontend development. 1 skill with 23 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.", "description": "Design fluency for frontend development. 1 skill with 24 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
"version": "4.4.0", "version": "4.3.1",
"author": { "author": {
"name": "Paul Bakaus", "name": "Paul Bakaus",
"email": "paul@paulbakaus.com" "email": "paul@paulbakaus.com"
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "impeccable", "name": "impeccable",
"description": "Design fluency for frontend development. 1 skill with 23 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.", "description": "Design fluency for frontend development. 1 skill with 24 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
"version": "4.4.0", "version": "4.3.1",
"author": { "author": {
"name": "Paul Bakaus", "name": "Paul Bakaus",
"email": "paul@paulbakaus.com" "email": "paul@paulbakaus.com"
@@ -18,10 +18,6 @@ Do not redesign. Preserve the reference's visual role, silhouette, palette, ligh
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 `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; 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; report a card too thin to brief a comp, never pad it 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 (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 keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. 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. 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 `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; 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; report a card too thin to brief a comp, never pad it 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 (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 keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. 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.
## Review handoff
After the initial production batch, return the actual files and any unresolved drift to the parent for the user-facing component review in [component-review.md](../reference/component-review.md). The parent includes rendered code regions alongside these rasters. A parent or automatic visual check is not a substitute for that human checkpoint. Apply requested repairs and preserve unchanged assets; do not self-approve them. This checkpoint does not apply to the Decision Comps job above.
## Input Contract ## Input Contract
Expect the measured spec (`.impeccable/build/spec.json`, written by `impeccable comp-spec` from the approved comp), the approved comp path, and the skill scripts path. Optionally: a subset of region ids to produce, extra prompt notes per region, and format or transparency needs. Everything else you need is in the spec: each raster region's id, kind (plate, image, texture), pixel box, sampled palette, aspect, note, and the plate path it must land on. Expect the measured spec (`.impeccable/build/spec.json`, written by `impeccable comp-spec` from the approved comp), the approved comp path, and the skill scripts path. Optionally: a subset of region ids to produce, extra prompt notes per region, and format or transparency needs. Everything else you need is in the spec: each raster region's id, kind (plate, image, texture), pixel box, sampled palette, aspect, note, and the plate path it must land on.
+1 -1
View File
@@ -21,7 +21,7 @@ Expect: the original request; the confirmed user answers; the artifact path(s);
## Checks, in order ## Checks, in order
0. **Evidence.** Before any other check, verify the required captures exist and every capture is valid. Required: the platform's full viewport set (web: `desktop.png` and `mobile.png`; native: one capture per shipped device class), plus every capture the calling brief names as required, a reported user viewport (`user-<width>.png`) included. Valid: no black or blank regions, content matching what the filename claims (a visit capture showing the About section is invalid), the document top visible where the file claims a full page, dimensions that make sense for the named viewport. A required capture that is absent fails exactly like one that is malformed: a viewport nobody captured is a viewport nobody inspected, and it cannot ship. When any capture fails, the whole review changes shape: return `disposition: recapture` as the first line, then one section, `recapture`, listing each missing or invalid file and what a valid capture of it shows, and stop. Never build a matrix on malformed evidence; a verdict derived from a broken capture launders the breakage into an approval, and the parent owes you a full re-review on valid captures, not a scoring round. 0. **Evidence.** Before any other check, verify the required captures exist and every capture is valid. Required: the platform's full viewport set (web: `desktop.png` and `mobile.png`; native: one capture per shipped device class), plus every capture the calling brief names as required, a reported user viewport (`user-<width>.png`) included. Valid: no black or blank regions, content matching what the filename claims (a visit capture showing the About section is invalid), the document top visible where the file claims a full page, dimensions that make sense for the named viewport. A required capture that is absent fails exactly like one that is malformed: a viewport nobody captured is a viewport nobody inspected, and it cannot ship. When any capture fails, the whole review changes shape: return `disposition: recapture` as the first line, then one section, `recapture`, listing each missing or invalid file and what a valid capture of it shows, and stop. Never build a matrix on malformed evidence; a verdict derived from a broken capture launders the breakage into an approval, and the parent owes you a full re-review on valid captures, not a scoring round.
1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and every phase before `review` is `closed` or explicitly `skipped`; an open or failed phase is a material finding. A comp-led config with no state file, or a state whose `comps` phase is neither `closed` nor `skipped`, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. 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, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all. 1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and its `comps` (or `skipped` when a surface round locked the comp), `spec`, `plates`, and `hero` phases are `closed`; a comp-led config with no state file, or a state whose `comps` phase never closed, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. 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, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all.
2. **Fidelity.** Start from the measurement, then judge what it cannot: read `.impeccable/review/diff/final/report.json` (and hero) first; every region scored `missing` or `contradicted` is a matrix row in that state unless the paired crop under `regions/` shows the score is wrong, and you say why; a region scored `match` still gets your eye for lettering character and material, which the numbers do not measure. Then, 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; 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. Three 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; medium is part of the promise. GROUND: the page field's value and temperature against the comp's, sampled from pixels on both sides when tooling allows rather than judged from memory, and read as the net on-screen result where a texture or tile paints over the base color; a ground warmer or cooler than the comp's is contradicted however faithfully the layout matches, and drift toward the rendition prior (warm cream on light grounds, blue-black slate on dark) is the direction to hunt. With no approved comp, 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 renders) as contradicted on its face; imitation material is the single most reliable mark of machine-made design. GROUND narrows rather than lapses: with no comp to sample, a color OWN-WORLD names is the target and the same warmer-or-cooler judgment applies; when OWN-WORLD names none, there is no GROUND authority, and the review says so in place of a verdict, because a target the reviewer invents turns the check into taste. A critique-reference comp on such a build is provocation, not spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is what the image dared that the build did not, and 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. A fix that requires producing an asset says so explicitly ("produce: <region> 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. 2. **Fidelity.** Start from the measurement, then judge what it cannot: read `.impeccable/review/diff/final/report.json` (and hero) first; every region scored `missing` or `contradicted` is a matrix row in that state unless the paired crop under `regions/` shows the score is wrong, and you say why; a region scored `match` still gets your eye for lettering character and material, which the numbers do not measure. Then, 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; 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. Three 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; medium is part of the promise. GROUND: the page field's value and temperature against the comp's, sampled from pixels on both sides when tooling allows rather than judged from memory, and read as the net on-screen result where a texture or tile paints over the base color; a ground warmer or cooler than the comp's is contradicted however faithfully the layout matches, and drift toward the rendition prior (warm cream on light grounds, blue-black slate on dark) is the direction to hunt. With no approved comp, 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 renders) as contradicted on its face; imitation material is the single most reliable mark of machine-made design. GROUND narrows rather than lapses: with no comp to sample, a color OWN-WORLD names is the target and the same warmer-or-cooler judgment applies; when OWN-WORLD names none, there is no GROUND authority, and the review says so in place of a verdict, because a target the reviewer invents turns the check into taste. A critique-reference comp on such a build is provocation, not spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is what the image dared that the build did not, and 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. A fix that requires producing an asset says so explicitly ("produce: <region> 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. 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, 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. 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, 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.
-12
View File
@@ -1,18 +1,6 @@
{ {
"description": "Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.", "description": "Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.",
"hooks": { "hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "[ ! -f \"${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/impeccable\" ] || \"${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/impeccable\" hook",
"timeout": 5,
"statusMessage": "Preparing build session"
}
]
}
],
"PostToolUse": [ "PostToolUse": [
{ {
"matcher": "Edit|Write", "matcher": "Edit|Write",
+4 -3
View File
@@ -1,9 +1,9 @@
--- ---
name: impeccable name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks. description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.4.0 version: 4.3.1
user-invocable: true user-invocable: true
argument-hint: "[shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]" argument-hint: "[shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live|generate] [target]"
license: Apache 2.0 license: Apache 2.0
--- ---
@@ -65,7 +65,8 @@ Choose the mode from the requested surface, not the product, and persist it only
| `clarify [target]` | Fix | Improve UX copy, labels, and error messages | [reference/clarify.md](reference/clarify.md) | | `clarify [target]` | Fix | Improve UX copy, labels, and error messages | [reference/clarify.md](reference/clarify.md) |
| `adapt [target]` | Fix | Adapt for different devices and screen sizes | [reference/adapt.md](reference/adapt.md) · native: [reference/adapt.native.md](reference/adapt.native.md) | | `adapt [target]` | Fix | Adapt for different devices and screen sizes | [reference/adapt.md](reference/adapt.md) · native: [reference/adapt.native.md](reference/adapt.native.md) |
| `optimize [target]` | Fix | Diagnose and fix UI performance | [reference/optimize.md](reference/optimize.md) | | `optimize [target]` | Fix | Diagnose and fix UI performance | [reference/optimize.md](reference/optimize.md) |
| `live` | Iterate | Visual variant mode: pick elements in the browser, generate alternatives | [reference/live.md](reference/live.md) | | `live` | Iterate | Visual variant mode: pick elements in the browser, iterate on alternatives | [reference/live.md](reference/live.md) |
| `generate [n] [action] [element]` | Iterate | Variants, versions, or alternatives of a named element to choose from in the live browser; no manual picking | [reference/generate.md](reference/generate.md) |
Routing: Routing:
@@ -188,6 +188,12 @@ Test thoroughly across contexts:
- **Edge cases**: Very small screens (320px), very large screens (4K) - **Edge cases**: Very small screens (320px), very large screens (4K)
- **Slow connections**: Test on throttled network - **Slow connections**: Test on throttled network
**Custom controls** (sliders, drag surfaces, scrollable control strips): a before/after slider can pass every width check above and still refuse to drag on iOS, so exercise each one in scope in the same batched round as the checks above:
- **Primary gesture**: Tap it and confirm it responds as designed, then drag it with the target input method; the drag must complete, not just start
- **Scroll across it**: A swipe along the page's scroll axis across the control scrolls the page or container without activating it; a drag that starts on the control along its axis moves the control, not the page. Neither failure throws an error, so try both
- **Evidence**: Say what produced the evidence: an emulated viewport, synthesized touch input through a browser tool, which engine ran it (Chromium is not Safari), or a physical device. Screenshots and resized viewports verify layout, never a gesture. Name what stayed untested and move on; unreachable hardware is a reported gap, not a blocker
When the adaptation feels native to each context, hand off to `/impeccable polish` for the final pass. When the adaptation feels native to each context, hand off to `/impeccable polish` for the final pass.
--- ---
+2 -1
View File
@@ -48,11 +48,12 @@ Run comprehensive checks across 5 dimensions. Score each dimension 0-4 using the
**Check for**: **Check for**:
- **Fixed widths**: Hard-coded widths that break on mobile - **Fixed widths**: Hard-coded widths that break on mobile
- **Touch targets**: Interactive elements < 44x44px - **Touch targets**: Interactive elements < 44x44px
- **Broken touch interaction**: Custom sliders, drag surfaces, and scrollable control strips whose primary gesture fails under touch, that swallow page scroll or lose the drag to it, or that stay stuck after an interrupted gesture. Code tells: mouse-only handlers, no `touch-action` on a pointer-event drag surface, drag state that nothing clears on cancel, lost capture, or blur. Exercise the gesture when a browser tool can synthesize touch (a rendered viewport proves layout, not the gesture), then say what produced the evidence (emulated viewport, synthesized touch, which engine, physical device) and what stayed untested
- **Horizontal scroll**: Content overflow on narrow viewports - **Horizontal scroll**: Content overflow on narrow viewports
- **Text scaling**: Layouts that break when text size increases - **Text scaling**: Layouts that break when text size increases
- **Missing breakpoints**: No mobile/tablet variants - **Missing breakpoints**: No mobile/tablet variants
**Score 0-4**: 0=Desktop-only (breaks on mobile), 1=Major issues (some breakpoints, many failures), 2=Partial (works on mobile, rough edges), 3=Good (responsive, minor touch target or overflow issues), 4=Excellent (fluid, all viewports, proper touch targets) **Score 0-4**: 0=Desktop-only (breaks on mobile), 1=Major issues (some breakpoints, many failures), 2=Partial (works on mobile, rough edges), 3=Good (responsive, minor touch target or overflow issues), 4=Excellent (fluid, all viewports, proper touch targets, gestures work under touch)
### 5. Implementation Integrity (CRITICAL) ### 5. Implementation Integrity (CRITICAL)
@@ -1,57 +0,0 @@
# Component review
Use this checkpoint on comp-led builds after producing the initial component kit and before composing the page. The approved comp is the reference. The user reviews the actual produced components, including code; a list of planned assets or screenshots supplied by the builder is not a review of what will ship.
## Prepare the component kit
Keep the measured spec's region IDs. Include every visible region: produced raster assets and working HTML/CSS/SVG for text, controls, patterns, decoration and layout elements. A region rendered in code needs an actual review document, not a promise to implement it later. Use semantic HTML for content and controls. Do not flatten the page or combine unrelated regions to avoid review. Report omitted regions so the user can mark what is missing.
Write `.impeccable/review/components.json` with this manifest format:
```json
{
"schemaVersion": 1,
"id": "components",
"title": "Component review",
"stage": "components",
"comp": {"path": ".impeccable/mocks/comp-2.png", "width": 1536, "height": 1024},
"components": [
{
"id": "illustration",
"name": "Illustration",
"medium": "raster",
"box": {"x": 0.5, "y": 0.2, "w": 0.45, "h": 0.7},
"note": "Produced cutout; positioned over the page ground.",
"preview": {"kind": "image", "path": "assets/illustration.png"},
"dependencies": [".impeccable/build/spec.json"]
},
{
"id": "headline",
"name": "Headline",
"medium": "html",
"box": {"x": 0.05, "y": 0.2, "w": 0.4, "h": 0.25},
"note": "Rendered semantic heading and its typography.",
"preview": {"kind": "page", "path": ".impeccable/review/components/headline.html"},
"dependencies": [".impeccable/build/spec.json", "assets/type.woff2"]
}
]
}
```
The coordinates above only illustrate the schema. Use the approved comp's actual pixel dimensions and each measured region's normalized bounds (`x / width`, `y / height`, `w / width`, `h / height`). A code preview is rendered at the comp viewport and cropped to that component's box, so place its content at those coordinates in the review document. Include every file the document uses in `dependencies`, including linked CSS, fonts and images. The runtime also binds the measured spec for the component stage and checks its inventory. Local paths only. Static PNG, WebP and JPEG previews retain their original bytes and actual transparency; never draw a checkerboard into the asset.
Native capture supports stable HTML/CSS and inline SVG. Supply a static review state for motion and keep the implementation's real inputs. A scripted, canvas or otherwise unsupported component is a blocker to report, not permission to substitute a raster or omit it.
## Present and wait
If the harness exposes `component_review`, call it with `manifest_path` set to `.impeccable/review/components.json`. The host captures the component files, presents this same review interface and returns the user's decisions. A suspended request is waiting for the user; it is not a failed build or an approval.
Otherwise run `.claude/skills/impeccable/scripts/impeccable component-review capture --manifest .impeccable/review/components.json`, then start `.claude/skills/impeccable/scripts/impeccable component-review serve --session <returned session>` in the background. Open the URL it prints in the available browser and wait for the user. Read the result with `.claude/skills/impeccable/scripts/impeccable component-review verify --manifest .impeccable/review/components.json`; pending, needs-work and stale input all refuse approval. Never submit the page or write a receipt on the user's behalf.
The user can approve components, request changes, and mark missing regions. Act on their feedback without replacing it with your own favorable verdict. Keep component IDs stable, update the actual implementation and dependency list, and present another round. The UI carries only approvals whose component inputs have not changed. Do not ask the user to reapprove unchanged work. Continue only when the inventory is confirmed and all components are approved.
## Assemble and review
Build the page from the approved component files. Replacing, simplifying or changing an approved component requires a new component review. Run the existing plates and hero gates; human review does not waive their integrity checks.
After the full page and responsive checks are complete, present a second manifest at `.impeccable/review/hero.json`, with `id` and `stage` set to `hero`. Use one page-preview component covering the assembled first viewport, its real HTML entry, and its complete dependency list. The reference stays the approved comp. Call the same host review tool (or native capture/serve/verify workflow) and obtain the user's approval before the final response. Later edits to the reviewed files require a fresh review. A component-kit approval does not approve their assembled layout.
@@ -13,10 +13,6 @@ Do not redesign. Preserve the reference's visual role, silhouette, palette, ligh
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 `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; 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; report a card too thin to brief a comp, never pad it 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 (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 keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. 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. 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 `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; 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; report a card too thin to brief a comp, never pad it 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 (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 keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. 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.
## Review handoff
After the initial production batch, return the actual files and any unresolved drift to the parent for the user-facing component review in [component-review.md](../reference/component-review.md). The parent includes rendered code regions alongside these rasters. A parent or automatic visual check is not a substitute for that human checkpoint. Apply requested repairs and preserve unchanged assets; do not self-approve them. This checkpoint does not apply to the Decision Comps job above.
## Input Contract ## Input Contract
Expect the measured spec (`.impeccable/build/spec.json`, written by `impeccable comp-spec` from the approved comp), the approved comp path, and the skill scripts path. Optionally: a subset of region ids to produce, extra prompt notes per region, and format or transparency needs. Everything else you need is in the spec: each raster region's id, kind (plate, image, texture), pixel box, sampled palette, aspect, note, and the plate path it must land on. Expect the measured spec (`.impeccable/build/spec.json`, written by `impeccable comp-spec` from the approved comp), the approved comp path, and the skill scripts path. Optionally: a subset of region ids to produce, extra prompt notes per region, and format or transparency needs. Everything else you need is in the spec: each raster region's id, kind (plate, image, texture), pixel box, sampled palette, aspect, note, and the plate path it must land on.
@@ -16,7 +16,7 @@ Expect: the original request; the confirmed user answers; the artifact path(s);
## Checks, in order ## Checks, in order
0. **Evidence.** Before any other check, verify the required captures exist and every capture is valid. Required: the platform's full viewport set (web: `desktop.png` and `mobile.png`; native: one capture per shipped device class), plus every capture the calling brief names as required, a reported user viewport (`user-<width>.png`) included. Valid: no black or blank regions, content matching what the filename claims (a visit capture showing the About section is invalid), the document top visible where the file claims a full page, dimensions that make sense for the named viewport. A required capture that is absent fails exactly like one that is malformed: a viewport nobody captured is a viewport nobody inspected, and it cannot ship. When any capture fails, the whole review changes shape: return `disposition: recapture` as the first line, then one section, `recapture`, listing each missing or invalid file and what a valid capture of it shows, and stop. Never build a matrix on malformed evidence; a verdict derived from a broken capture launders the breakage into an approval, and the parent owes you a full re-review on valid captures, not a scoring round. 0. **Evidence.** Before any other check, verify the required captures exist and every capture is valid. Required: the platform's full viewport set (web: `desktop.png` and `mobile.png`; native: one capture per shipped device class), plus every capture the calling brief names as required, a reported user viewport (`user-<width>.png`) included. Valid: no black or blank regions, content matching what the filename claims (a visit capture showing the About section is invalid), the document top visible where the file claims a full page, dimensions that make sense for the named viewport. A required capture that is absent fails exactly like one that is malformed: a viewport nobody captured is a viewport nobody inspected, and it cannot ship. When any capture fails, the whole review changes shape: return `disposition: recapture` as the first line, then one section, `recapture`, listing each missing or invalid file and what a valid capture of it shows, and stop. Never build a matrix on malformed evidence; a verdict derived from a broken capture launders the breakage into an approval, and the parent owes you a full re-review on valid captures, not a scoring round.
1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and every phase before `review` is `closed` or explicitly `skipped`; an open or failed phase is a material finding. A comp-led config with no state file, or a state whose `comps` phase is neither `closed` nor `skipped`, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. 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, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all. 1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and its `comps` (or `skipped` when a surface round locked the comp), `spec`, `plates`, and `hero` phases are `closed`; a comp-led config with no state file, or a state whose `comps` phase never closed, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. 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, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all.
2. **Fidelity.** Start from the measurement, then judge what it cannot: read `.impeccable/review/diff/final/report.json` (and hero) first; every region scored `missing` or `contradicted` is a matrix row in that state unless the paired crop under `regions/` shows the score is wrong, and you say why; a region scored `match` still gets your eye for lettering character and material, which the numbers do not measure. Then, 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; 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. Three 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; medium is part of the promise. GROUND: the page field's value and temperature against the comp's, sampled from pixels on both sides when tooling allows rather than judged from memory, and read as the net on-screen result where a texture or tile paints over the base color; a ground warmer or cooler than the comp's is contradicted however faithfully the layout matches, and drift toward the rendition prior (warm cream on light grounds, blue-black slate on dark) is the direction to hunt. With no approved comp, 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 renders) as contradicted on its face; imitation material is the single most reliable mark of machine-made design. GROUND narrows rather than lapses: with no comp to sample, a color OWN-WORLD names is the target and the same warmer-or-cooler judgment applies; when OWN-WORLD names none, there is no GROUND authority, and the review says so in place of a verdict, because a target the reviewer invents turns the check into taste. A critique-reference comp on such a build is provocation, not spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is what the image dared that the build did not, and 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. A fix that requires producing an asset says so explicitly ("produce: <region> 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. 2. **Fidelity.** Start from the measurement, then judge what it cannot: read `.impeccable/review/diff/final/report.json` (and hero) first; every region scored `missing` or `contradicted` is a matrix row in that state unless the paired crop under `regions/` shows the score is wrong, and you say why; a region scored `match` still gets your eye for lettering character and material, which the numbers do not measure. Then, 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; 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. Three 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; medium is part of the promise. GROUND: the page field's value and temperature against the comp's, sampled from pixels on both sides when tooling allows rather than judged from memory, and read as the net on-screen result where a texture or tile paints over the base color; a ground warmer or cooler than the comp's is contradicted however faithfully the layout matches, and drift toward the rendition prior (warm cream on light grounds, blue-black slate on dark) is the direction to hunt. With no approved comp, 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 renders) as contradicted on its face; imitation material is the single most reliable mark of machine-made design. GROUND narrows rather than lapses: with no comp to sample, a color OWN-WORLD names is the target and the same warmer-or-cooler judgment applies; when OWN-WORLD names none, there is no GROUND authority, and the review says so in place of a verdict, because a target the reviewer invents turns the check into taste. A critique-reference comp on such a build is provocation, not spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is what the image dared that the build did not, and 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. A fix that requires producing an asset says so explicitly ("produce: <region> 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. 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, 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. 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, 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.
@@ -0,0 +1,101 @@
> **Additional context needed**: only the target element, when the request does not name one that resolves uniquely on the page.
Generate is the fast lane into live mode: the user names an element, a direction, and a count in one sentence, and within a minute they are cycling through variants in their browser. One command boots the helper, hands the element to the overlay in the page your harness already shows (it scrolls to it, selects it, and fires the same Go a click fires) and returns the generate event; one edit writes the variants; one call replies and waits for the user's choice, which the helper bakes into source itself. This file owns the lane's plumbing; from the event onward the design work is [live.md](live.md)'s, unchanged, so read it in full now if you have not this session.
**Web only.** Live mode's browser overlay has no native equivalent; on `ios` / `android` / `adaptive` projects, decline this command and offer `bolder` or `quieter` on the source instead.
The plumbing is where the lane saves time: one command starts the session around the page your harness already shows, one call replies and waits, and nothing here is a browser you have to babysit. The design work is not where it saves time. Setup runs as for any command (`impeccable context`, this reference, craft-floor.md before the edit), and the variants are planned, written, and accepted exactly the way a live session plans, writes, and accepts them.
Three prohibitions cover the known ways this command goes wrong:
- **Never run init or document, and never ask for PRODUCT.md or DESIGN.md.** When they exist, the start command prints them under `boot` and you use them. When they do not, it says so (`contextMissing`, `contextNote`) and you extract the identity from the event (Step 3). A missing file is never a reason to interview the user inside this command; offer `init` in one line after the session ends.
- **Never hand-write a variants wrapper or invent a session id.** Only the browser mints session ids (8 hex characters, at Go). A missing event is fixed by rerunning Step 2, never with a direct source edit.
- **Do not act on hook findings while live markers are in the file**, and do not restyle variants to appease them; the accept verifies the file once the variant is permanent.
## Step 1: Parse the request
Three parts, all from the user's sentence:
- **A number in the request**: that is the count. **No number**: 3. The protocol caps count at 8.
- **The direction wording** maps onto the live action vocabulary; never invent a new action value:
- **bold, bolder, stronger, punchier**: `bolder`
- **quiet, calmer, softer, toned down**: `quieter`
- **simpler, minimal, stripped**: `distill`
- **refined, tightened, polished**: `polish`
- **font and type words**: `typeset`
- **color words**: `colorize`
- **arrangement and spacing words**: `layout`
- **device and breakpoint words**: `adapt`
- **motion words**: `animate`
- **playful words**: `delight`
- **rule-breaking words**: `overdrive`
- **Wording that carries intent but no vocabulary word** ("make it feel like a bank", "warmer", "more premium"): `impeccable`, with the user's wording passed as the prompt.
- **An action fits AND extra intent rides along** ("bolder, but keep it monochrome"): that action, with the rest as the prompt.
- **The wording names no direction at all** ("better", "improve", "nicer", "different", "fresh", "new", "redesign", "fix", "some options", "ideas", "alternatives", or just "variants" with nothing else): STOP and call the AskUserQuestion tool to clarify. Ask one question, offering the vocabulary: *"Which direction should the variants take? bolder, quieter, simpler (distill), polished, typography (typeset), color (colorize), layout, motion (animate), playful (delight), or rule-breaking (overdrive)."* Map the answer with this list; an answer that is still open ("surprise me", "you pick") is `impeccable` with the user's original wording as the prompt, and Step 2 starts on that answer.
- **The element description** ("the pricing cards", "the hero heading"): Step 2 resolves it to a selector.
Done when you hold an action from the vocabulary (asked for, when the request named no direction), a count from 1 to 8, and the element description.
## Step 2: Reuse the page, then start
**Reuse** the dev server already running and the tab your harness already shows it in; a second server or a second browser window is the failure this step prevents.
1. **Find the dev server**, cheapest source first, and stop at the first hit: the user's message, a browser tab already on the app (Claude Code: an origin in `tabs_context`), a server your harness started (Claude Code: `preview_list`), a terminal that printed its URL. Its origin is your `--dev-url`. **No hit**: leave `--dev-url` off and run the start command with no wait; the boot probes for a running server and its verdict names the move. `browser_needed` carries the `devUrl` it found: open it as in 2, then rerun with `--dev-url <devUrl> --wait-for-browser 60000`. `no_dev_server` means nothing serves the app: start the dev script the way the verdict says (Claude Code: `preview_start`; Cursor: a background terminal; Codex: an exec you yield from), wait for its URL, then rerun with `--dev-url <url>`.
2. **Open the page that renders the element in your browser, then start.** The route the request names, else the one `--target` serves; `--dev-url` takes only the origin.
- **Cursor** (`browser_navigate`) and **Claude Code** (`navigate`, which opens the Browser pane when it is closed and takes the `tabId` from `tabs_context` when a tab is already on that origin): open the URL, then run the start command with `--dev-url <url> --wait-for-browser 60000`. The boot injects the overlay and the page reloads into it while the command waits. Your browser tool is the only opener on these harnesses; the engine ignores `--open` there.
- **No browser tool** (Codex, others): run the start command with `--open --wait-for-browser 120000`; it opens the system browser, and the longer wait covers the user finding the tab. **`browser_open_failed` back**: tell the user the `url` in one line and rerun with `--wait-for-browser 120000`.
```bash
.claude/skills/impeccable/scripts/impeccable live-generate --target src/App.jsx --dev-url http://127.0.0.1:5173/ --selector ".pricing-grid" --action bolder --count 3 --boot --wait-for-browser 60000
```
Run it in the foreground in Cursor and Claude Code (it returns within the wait); on Codex, in an exec you yield from, the way Step 3 runs the poll.
- `--target`: the file that renders the element when the request or the project makes it obvious; skip it otherwise.
- `--dev-url`: the origin from 1; omit it and the boot probes.
- `--selector`: a unique class first, then a landmark tag plus class, an id last (every variant mounts a copy of the element, so an id repeats in the DOM). **The request names a repeated component in plural** ("the pricing cards"): target the container that holds the set, so one scoped stylesheet restyles every instance. One read of the source file that renders the element is allowed when the selector is not obvious; `--dry-run` resolves and reports without starting anything when it is not certain.
- `--boot`: runs the lane's boot (PRODUCT.md and DESIGN.md loaded again for the helper, missing files tolerated, dev URL found, bottom bar hidden for the helper's lifetime) and reuses a helper that is already running. Its result rides along as `boot`.
- Also available: `--prompt`, `--text` (keep only matches whose visible text contains a snippet), `--index` (1-based pick among matches).
Read the output in this order: `boot` (or `boot.contextMissing` with `boot.contextNote`: the page is the source of truth, per the note), then `event`, the generate event for `sessionId`, with the same `_instructions` a user's Go gets. Every verdict carries `_instructions`, and they win over your recollection of this file; the ones whose move is a decision of yours:
- **`ambiguous`**: the candidates are listed; target their common container, or rerun with `--text "<visible text>"` or `--index <n>`.
- **`dev_server_gone`**: the dev server stopped answering while the command waited for the page (on Cursor, a server another chat started dies with that chat). Start it the way the verdict says, then rerun with `--dev-url <url>`.
- **`no_match`**: the tab is on a route that does not render the element (navigate to the right route, rerun), or the selector is wrong (derive a better one from the source, or add `--text`).
- **`config_missing` / `config_invalid`** under `bootError`: follow [live-setup.md](live-setup.md) first, then rerun.
- **`event: null`** with `ok: true`: the event was slower than the wait; run `.claude/skills/impeccable/scripts/impeccable live-poll` once to collect it, then continue.
Done when the output shows `ok: true`, a `sessionId`, and an `event`, reached with at most one server started and one tab opened by you.
## Step 3: Generate
The event is a standard `generate` event: the picked element's context, a preflighted scaffold, and `_instructions` naming the action's reference, the planning section, and the exact splice. Handle it exactly per live.md's **Handle generate**, which owns everything from the identity lock to the done reply: read the action's reference and craft-floor.md as it says, plan per section 4 (identity first, then mode, then three different primary axes, then the squint test), declare knobs per section 7, and deliver per section 6 (a complete replacement of the element per variant, the preview CSS plus every variant in one edit at the scaffold's splice). The lane changes nothing about what a variant may be: the moves a live session would make on this element (a promoted tier, a restructured set, a reordered card, a different surface) are open here too. Never screenshot the page; the overlay preview is the review channel until accept.
**Reply and wait in one call**, with the file you wrote:
```bash
.claude/skills/impeccable/scripts/impeccable live-poll --reply EVENT_ID done --file src/App.jsx --then-poll
```
This replies done (the browser mounts the variants) and then blocks until the user's choice arrives, so run it the way your harness runs a long wait: **Claude Code** in the foreground with your tool's longest timeout (600000 ms), so you are paused until the choice arrives; **Codex** in a yielded foreground exec; **Cursor** in a background terminal with notify on `"type":"(accept|discard|variant_mount_failed|exit)"`. Never pass a short `--timeout=`. While it runs there is nothing else to do: never sleep and never poll its output on a timer; a harness that backgrounds it wakes you when it returns. `{"type":"timeout"}` means the user has not chosen yet: run `live-poll` again and keep waiting. If the edit fails after the browser flipped to GENERATING, `--reply EVENT_ID error "Short reason"` (without `--then-poll`) so the bar resets.
Then tell the user, in one line, where their variants are: *"Three [bolder] variants are live on [the pricing cards]: cycle with the floating bar's arrows, adjust the Tune knobs, and Accept the keeper."*
Outside the replace path, read the matching live.md section before acting: `scaffold.previewMode: "svelte-component"` (Svelte previews are edited as components, and their accept is mechanical), `mode: "insert"`, `variant_mount_failed`, `steer`, `manual_edit_apply`, and any `fallback: "agent-driven"` wrap error.
## Step 4: Accept and close
The call from Step 3 returns the user's choice. **`discard`**: nothing to do. **`accept`**: `_acceptResult.carbonize: true` is the normal case, and the cleanup is live.md's **Required after accept**, unchanged: move the accepted variant's rules into the stylesheet that already owns the element with real selectors, bake the chosen knob values in, unwrap the element and drop every `data-impeccable-*` attribute, delete the inline `<style>` block and both `impeccable-carbonize` markers, then `.claude/skills/impeccable/scripts/impeccable live-complete --id SESSION_ID` and confirm `phase: "completed"`. (`baked: true` appears only when the accept was run with `--bake`; then the helper already made the variant permanent and no `live-complete` is owed.)
Close without being asked, the moment the choice is handled:
```bash
.claude/skills/impeccable/scripts/impeccable live-server stop
```
Stopping removes the injected script and reloads the page once: the user sees the accepted design with no overlay chrome, still served by their dev server. **Never kill or restart the dev server**, including one you started in Step 2.
- **The user asks for more variants before you closed**: skip the close, run Step 2 again for the next element (the helper is reused), and close after the last choice.
- **Interrupted or unsure of the state**: `.claude/skills/impeccable/scripts/impeccable live-status`, then `live-resume`; the journal under `.impeccable/live/sessions/` is canonical.
Done when the helper is stopped and the dev site still answers with the accepted design.
@@ -205,6 +205,11 @@ t('items', { count }) // Handles complex plural rules
- Optimistic updates with rollback - Optimistic updates with rollback
- Conflict resolution - Conflict resolution
**Interrupted gestures** (custom sliders, drag surfaces, scrollable control strips):
- A second finger or pointer lands mid-drag: the first drag keeps its pointer or ends cleanly, never jumps to the new one
- The browser cancels the gesture to scroll (`pointercancel`), capture is lost (`lostpointercapture`), the pointer is released outside the control, or the window loses focus (`blur`) mid-drag: clear the dragging state and release capture
- After each of these, the next tap or drag works without a reload
**Permission states**: **Permission states**:
- No permission to view - No permission to view
- No permission to edit - No permission to edit
@@ -304,6 +309,7 @@ const throttledScroll = throttle(handleScroll, 100);
- Unit tests for edge cases - Unit tests for edge cases
- Integration tests for error scenarios - Integration tests for error scenarios
- E2E tests for critical paths - E2E tests for critical paths
- A behavioral regression for each confirmed gesture fix, when the project's test runner can drive input
- Visual regression tests - Visual regression tests
- Accessibility tests (axe, WAVE) - Accessibility tests (axe, WAVE)
@@ -330,7 +336,10 @@ Test thoroughly with edge cases:
- **Network issues**: Disable internet, throttle connection - **Network issues**: Disable internet, throttle connection
- **Large datasets**: Test with 1000+ items - **Large datasets**: Test with 1000+ items
- **Concurrent actions**: Click submit 10 times rapidly - **Concurrent actions**: Click submit 10 times rapidly
- **Interrupted gestures**: Add a second finger mid-drag, scroll across the control, release outside it, switch windows mid-drag; then drag again
- **Errors**: Force API errors, test all error states - **Errors**: Force API errors, test all error states
- **Empty**: Remove all data, test empty states - **Empty**: Remove all data, test empty states
For gestures, say what produced the evidence (emulated viewport, synthesized touch, which engine, physical device) and name what stayed untested.
When edge cases are covered, hand off to `/impeccable polish` for the final pass. When edge cases are covered, hand off to `/impeccable polish` for the final pass.
@@ -98,7 +98,7 @@ Build the assigned direction, not a safer interpretation of it. The form supplie
When an approved comp exists, it is a spatial contract, not a mood board: only the user can downgrade its authority, in explicit words. Models systematically believe their HTML, CSS, and SVG recreation of an image succeeded when it did not, so the build runs as a state machine on disk whose gates measure the screen against the comp instead of asking you to remember it. Start it once, and let it tell you what is next: When an approved comp exists, it is a spatial contract, not a mood board: only the user can downgrade its authority, in explicit words. Models systematically believe their HTML, CSS, and SVG recreation of an image succeeded when it did not, so the build runs as a state machine on disk whose gates measure the screen against the comp instead of asking you to remember it. Start it once, and let it tell you what is next:
`.claude/skills/impeccable/scripts/impeccable build-phase start --direction <seed key> --kind <assigned|pick|challenger|canon> --artifact <entry file>` right after the direction choice (this is also the choice ping; the roll's output names the exact command), or `start --comp <approved comp> --artifact <entry file>` when a surface round already locked one. `.claude/skills/impeccable/scripts/impeccable build-phase start --direction <seed key> --kind <assigned|pick|challenger|canon>` right after the direction choice (this is also the choice ping; the roll's output names the exact command), or `start --comp <approved comp>` when a surface round already locked one.
Then, in order, each closed by `.claude/skills/impeccable/scripts/impeccable build-phase advance` (every verb below runs as `.claude/skills/impeccable/scripts/impeccable <verb>`; exit 2 means the gate failed and printed why; fix that and advance again; write nothing for a later phase while an earlier gate is open): Then, in order, each closed by `.claude/skills/impeccable/scripts/impeccable build-phase advance` (every verb below runs as `.claude/skills/impeccable/scripts/impeccable <verb>`; exit 2 means the gate failed and printed why; fix that and advance again; write nothing for a later phase while an earlier gate is open):
@@ -106,9 +106,8 @@ Then, in order, each closed by `.claude/skills/impeccable/scripts/impeccable bui
The comp-led path is a frontier-tier job: it asks the builder to hold a measured layout, place plates at their boxes, and act on numeric readings across a dozen attempts. Smaller or faster models produce a recognisable page and stall under the hero gate; if the model in hand is one of those, say so before the direction round and take the code-led path, or expect the run to end at the hero with its readings unmet. The comp-led path is a frontier-tier job: it asks the builder to hold a measured layout, place plates at their boxes, and act on numeric readings across a dozen attempts. Smaller or faster models produce a recognisable page and stall under the hero gate; if the model in hand is one of those, say so before the direction round and take the code-led path, or expect the run to end at the hero with its readings unmet.
1. **spec.** Measure the comp: `impeccable comp-spec --comp <comp> --grid` writes a coordinate grid over the comp; open it, name every salient region by grid span in a regions file (text and control regions snap to the largest ink mass inside their span, so a headline named B1:E4 measures as the headline and not the column beside it; `snap: false` keeps the span, and an explicit `box` is taken as drawn) (kind `plate` / `image` / `texture` for anything painted: every illustration, photograph, figure, product object, and material texture; `text` / `control` / `chrome` for what code draws; every region carries a `note` saying what the comp shows there, which the plate prompt and the gate messages read), and run `impeccable comp-spec --comp <comp> --regions <file>`. The spec carries each region's box, sampled palette, and medium; `impeccable comp-spec --print` is the build's reference from here on. Type is measured, not guessed: `impeccable font-match --measure <text region>` reads the comp's cap height, width class, and weight off the pixels, and `impeccable font-match --rank <region> --text "..."` takes its candidates from a fingerprint index of the Google Fonts catalog (the nearest faces to the crop's shape) plus any names you pass with `--candidates`, renders them at that cap height with the region's words, and ranks them by fingerprint distance (its `USE` line is the CSS; its proof sheet shows the comp over the top three); with no browser resolvable it records the catalog's nearest face and says the size is estimated, which is still the choice to build on. Do not install a browser to rank, and never write a `chosen` face into the spec by hand: the gate accepts only what font-match wrote. The spec gate refuses to close until the lead text region is measured and ranked. A region note that describes painted material (a diagram, drawing, photograph, texture) under a code kind is refused at the spec: reclassify it as a plate, or reword the note if code really draws it. The script refuses a regions file that leaves comp ink unnamed (callouts, a parts table, a notes block): what is never named can never be missing, so everything the comp shows gets a region. It also refuses a `text` / `control` / `chrome` region larger than a quarter of the comp: that is a column, not an element, and a column scored as one region hides the plates, tables, and notes inside it. Name each element inside it (`container: true` only when it truly is one undivided element). Anything drawn is a plate: an inline SVG past an icon's budget (a diagram, notation, leader lines with arrows, a "quick approximation" of the artwork) is refused at the hero; icon-sized SVG (under 64px, a few paths) is fine, and a chart the page draws from data at runtime is a chart, not an illustration. Callout lines and arrows that annotate a drawing belong to that drawing's plate, with only their labels set as text. A crop of the comp is never a plate (the plates gate refuses a file that is a resample of the comp region: the comp's grain, its neighbours' edges, and its resolution would ship as the artwork); the crop is the reference the plate is generated from. A plate region's box has to hold its whole artwork with a margin: the spec measures the artwork's contact with the box edges and refuses a box that cuts through it (`bleed: true` only when the page really crops it there), because a plate placed with `object-fit: cover` on such a box shows the artwork minus the side the box lost. Anything not in the spec does not exist on the page: no borders, rules, containers, or chrome the comp does not show. Only three concessions exist: fonts (the closest obtainable face), icon glyphs (close enough, exact if the user chose an icon library; this covers the pictogram only, never a control's chrome, so a chevron, an arrow, a dropdown's border and fill, a button's shape are the comp's), and genuine defects in the comp such as spelling errors. 1. **spec.** Measure the comp: `impeccable comp-spec --comp <comp> --grid` writes a coordinate grid over the comp; open it, name every salient region by grid span in a regions file (text and control regions snap to the largest ink mass inside their span, so a headline named B1:E4 measures as the headline and not the column beside it; `snap: false` keeps the span, and an explicit `box` is taken as drawn) (kind `plate` / `image` / `texture` for anything painted: every illustration, photograph, figure, product object, and material texture; `text` / `control` / `chrome` for what code draws; every region carries a `note` saying what the comp shows there, which the plate prompt and the gate messages read), and run `impeccable comp-spec --comp <comp> --regions <file>`. The spec carries each region's box, sampled palette, and medium; `impeccable comp-spec --print` is the build's reference from here on. Type is measured, not guessed: `impeccable font-match --measure <text region>` reads the comp's cap height, width class, and weight off the pixels, and `impeccable font-match --rank <region> --text "..."` takes its candidates from a fingerprint index of the Google Fonts catalog (the nearest faces to the crop's shape) plus any names you pass with `--candidates`, renders them at that cap height with the region's words, and ranks them by fingerprint distance (its `USE` line is the CSS; its proof sheet shows the comp over the top three); with no browser resolvable it records the catalog's nearest face and says the size is estimated, which is still the choice to build on. Do not install a browser to rank, and never write a `chosen` face into the spec by hand: the gate accepts only what font-match wrote. The spec gate refuses to close until the lead text region is measured and ranked. A region note that describes painted material (a diagram, drawing, photograph, texture) under a code kind is refused at the spec: reclassify it as a plate, or reword the note if code really draws it. The script refuses a regions file that leaves comp ink unnamed (callouts, a parts table, a notes block): what is never named can never be missing, so everything the comp shows gets a region. It also refuses a `text` / `control` / `chrome` region larger than a quarter of the comp: that is a column, not an element, and a column scored as one region hides the plates, tables, and notes inside it. Name each element inside it (`container: true` only when it truly is one undivided element). Anything drawn is a plate: an inline SVG past an icon's budget (a diagram, notation, leader lines with arrows, a "quick approximation" of the artwork) is refused at the hero; icon-sized SVG (under 64px, a few paths) is fine, and a chart the page draws from data at runtime is a chart, not an illustration. Callout lines and arrows that annotate a drawing belong to that drawing's plate, with only their labels set as text. A crop of the comp is never a plate (the plates gate refuses a file that is a resample of the comp region: the comp's grain, its neighbours' edges, and its resolution would ship as the artwork); the crop is the reference the plate is generated from. A plate region's box has to hold its whole artwork with a margin: the spec measures the artwork's contact with the box edges and refuses a box that cuts through it (`bleed: true` only when the page really crops it there), because a plate placed with `object-fit: cover` on such a box shows the artwork minus the side the box lost. Anything not in the spec does not exist on the page: no borders, rules, containers, or chrome the comp does not show. Only three concessions exist: fonts (the closest obtainable face), icon glyphs (close enough, exact if the user chose an icon library; this covers the pictogram only, never a control's chrome, so a chevron, an arrow, a dropdown's border and fill, a button's shape are the comp's), and genuine defects in the comp such as spelling errors.
2. **plates.** Every raster region ships as a plate: an illustration, photo, or figure regenerated at asset resolution from its comp crop, UI text removed, at its `plate` path (isolated ink, figures, or objects use native transparent PNG so they sit on the page's own ground; photos and textures stay opaque); a texture (paper, cloth, grain) is a clean patch of the comp region mirror-tiled to size, generated only when no clean patch exists. `impeccable comp-spec --crop <id>` writes the reference; save `impeccable comp-spec --plate-prompt <id> --background transparent` to a prompt file for a cutout, or use `--background opaque` otherwise. Prefer the harness-native image tool with that crop and prompt, then `impeccable embed-prompt <plate> --prompt-file <prompt.txt>`. The API fallback is `impeccable generate-image --ref <crop.png> --prompt-file <prompt.txt> --out <plate.png> --size <WxH> --quality high --background transparent` (use `--background opaque` for full-frame assets); create the output directory first. Verify actual alpha, white foregrounds, fine edges, and clear holes on light and dark grounds; do not chroma-key native output. After the initial assets exist, prepare the isolated code component previews and complete [component-review.md](component-review.md) before further gate-driven repair: the user reviews the whole component kit, including code, before page assembly. This checkpoint keeps the existing gates; it does not require passing them first. After the full page and responsive checks, use the assembled-hero checkpoint before the final response. The plates gate scores the assets against the comp; also inspect placement and scale visually. With parallel subagents, spawn the shipped asset producer (`impeccable-asset-producer`; `impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent") with the spec path and let it produce them all; without subagents, produce them here. A crop of the comp is a reference, never a shipping pixel. The gate checks every plate exists, is at least 1.5x the region's size, and reads as the region. Page code waits for this gate: a page written before its plates exist is a page that draws its material in CSS. A single-file deliverable changes nothing here: the plate is produced the same way and inlined as a data URI. `--force` exists for one case only, the user downgrading the comp's authority in words you quote in `--reason`; the script refuses every other reason. 2. **plates.** Every raster region ships as a plate: an illustration, photo, or figure regenerated at asset resolution from its comp crop, UI text removed, at its `plate` path (isolated ink, figures, or objects use native transparent PNG so they sit on the page's own ground; photos and textures stay opaque); a texture (paper, cloth, grain) is a clean patch of the comp region mirror-tiled to size, generated only when no clean patch exists. `impeccable comp-spec --crop <id>` writes the reference; save `impeccable comp-spec --plate-prompt <id> --background transparent` to a prompt file for a cutout, or use `--background opaque` otherwise. Prefer the harness-native image tool with that crop and prompt, then `impeccable embed-prompt <plate> --prompt-file <prompt.txt>`. The API fallback is `impeccable generate-image --ref <crop.png> --prompt-file <prompt.txt> --out <plate.png> --size <WxH> --quality high --background transparent` (use `--background opaque` for full-frame assets); create the output directory first. Verify actual alpha, white foregrounds, fine edges, and clear holes on light and dark grounds; do not chroma-key native output. The plates gate scores the assets against the comp; also inspect placement and scale visually. With parallel subagents, spawn the shipped asset producer (`impeccable-asset-producer`; `impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent") with the spec path and let it produce them all; without subagents, produce them here. A crop of the comp is a reference, never a shipping pixel. The gate checks every plate exists, is at least 1.5x the region's size, and reads as the region. Page code waits for this gate: a page written before its plates exist is a page that draws its material in CSS. A single-file deliverable changes nothing here: the plate is produced the same way and inlined as a data URI. `--force` exists for one case only, the user downgrading the comp's authority in words you quote in `--reason`; the script refuses every other reason.
3. **hero.** `impeccable build-phase scaffold` first: it writes the measured layout as CSS custom properties (`.impeccable/build/scaffold/layout.css`: `--r-<id>-x/y/w/h` in % of the comp, plus cap height, font-size, family, and weight where measured) and a reference page (`hero-reference.html`) with every region at its box and every plate placed. Bind the numbers to your own semantic structure, an element per region; the reference is a check on positions, never the page, and overlapping boxes are overlapping boxes. Then build only the first viewport, at the comp's own dimensions, the comp's words copied verbatim (the user approved that comp with those words; rewording is a stated decision after the hero passes, never a silent one inside it), every text region sized from its measured cap height and set in its ranked face, plates first: place every plate at its spec box (`object-fit: cover`, an `<img>`, a background image, or an inlined data URI named for it) before any text or control, capture into `.impeccable/review/hero-repro.png`, run `impeccable build-phase record hero` once so you see the plate regions read as match before any text exists, then lay the semantic layer over the plates from the spec's palette and boxes and advance. The gate first refuses while any plate is unreferenced by the source, then runs `impeccable comp-diff`, writes `.impeccable/review/diff/hero/` (side-by-side, heatmap, one paired crop per region, `report.json`), and passes at 72% overall with no hard veto outstanding (a missing region, a contradicted plate or text block, an SVG illustration, a clipped plate, invented ink block at any score); above the bar, the numeric readings become advisories printed with the pass, and the polish pass before responsive is where they get fixed: the gate also reads each text region's cap height, line count, weight, ink colour, and position against the comp, each chrome strip's height off its rule, and the frame for ink where the comp is calm (a kicker, an extra nav item, a divider), and says each miss as a number ("cap height 78px in the build, 103px in the comp"); those numbers are the edit. When it fails, open the region crops it lists, in order, before editing: a region scored `missing` needs its material, `contradicted` needs its structure re-derived from the spec box, `drift` is where size and spacing edits belong; the gate refuses a third attempt that only nudges values on the same region. This is where the run's ambition is won or lost, and a retry here costs minutes where a rebuild verdict at the finish costs the run.
3. **hero.** `impeccable build-phase scaffold` first: it writes the measured layout as CSS custom properties (`.impeccable/build/scaffold/layout.css`: `--r-<id>-x/y/w/h` in % of the comp, plus cap height, font-size, family, and weight where measured) and a reference page (`hero-reference.html`) with every region at its box and every plate placed. Bind the numbers to your own semantic structure, an element per region; the reference is a check on positions, never the page, and overlapping boxes are overlapping boxes. Then build only the first viewport, at the comp's own dimensions, the comp's words copied verbatim (the user approved that comp with those words; rewording is a stated decision after the hero passes, never a silent one inside it), every text region sized from its measured cap height and set in its ranked face, plates first: place every plate at its spec box (`object-fit: cover`, an `<img>`, a background image, or an inlined data URI named for it) before any text or control, capture into `.impeccable/review/hero-repro.png`, run `impeccable build-phase record hero` once so you see the plate regions read as match before any text exists, then lay the semantic layer over the plates from the spec's palette and boxes and advance. The gate first refuses while any plate is unreferenced by the source, then runs `impeccable comp-diff`, writes `.impeccable/review/diff/hero/` (side-by-side, heatmap, one paired crop per region, `report.json`; `raw-report.json` preserves the uninterpreted measurements). The report and crop labels use the gate's verdicts; `gate.reasons` lists the remaining blockers even when a region is called drift. An accepted plate is revalidated if its file, measured region, or comp changes. The gate passes at 72% overall with no hard veto outstanding (a missing region, a contradicted plate or text block, an SVG illustration, a clipped plate, invented ink block at any score); above the bar, the numeric readings become advisories printed with the pass, and the polish pass before responsive is where they get fixed: the gate also reads each text region's cap height, line count, weight, ink colour, and position against the comp, each chrome strip's height off its rule, and the frame for ink where the comp is calm (a kicker, an extra nav item, a divider), and says each miss as a number ("cap height 78px in the build, 103px in the comp"); those numbers are the edit. When it fails, open the region crops it lists, in order, before editing: a region scored `missing` needs its material, `contradicted` needs its structure re-derived from the spec box, `drift` is where size and spacing edits belong; repeated attempts do not clear unresolved blockers. This is where the run's ambition is won or lost, and a retry here costs minutes where a rebuild verdict at the finish costs the run.
4. **sections.** Build the rest of the surface inside the spec's system: the same corner language, line weights, and palette, and nothing the comp never shows. Where the comp does not cover a region, it inherits the recorded system. 4. **sections.** Build the rest of the surface inside the spec's system: the same corner language, line weights, and palette, and nothing the comp never shows. Where the comp does not cover a region, it inherits the recorded system.
5. **motion.** The signature interaction, reveals, and motion, orchestrated once rather than scattered. 5. **motion.** The signature interaction, reveals, and motion, orchestrated once rather than scattered.
6. **responsive.** The other viewports, and the first viewport at common desktop widths (1280 to 1600), not only at the comp's exact size: fluid columns, no fixed-pixel grid that wraps a hundred pixels narrower. Capture `desktop.png` (1440 wide, full page) and `mobile.png` (390 wide) into `.impeccable/review/`; the gate diffs the desktop capture against the comp and refuses a first viewport that only held at the comp's width. A comp'd surface that is mobile-first was comped portrait; the plates were produced for that frame. 6. **responsive.** The other viewports, and the first viewport at common desktop widths (1280 to 1600), not only at the comp's exact size: fluid columns, no fixed-pixel grid that wraps a hundred pixels narrower. Capture `desktop.png` (1440 wide, full page) and `mobile.png` (390 wide) into `.impeccable/review/`; the gate diffs the desktop capture against the comp and refuses a first viewport that only held at the comp's width. A comp'd surface that is mobile-first was comped portrait; the plates were produced for that frame.
@@ -146,5 +145,3 @@ A rebuild and a fix round share one asset rule: a raster either round creates or
Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice.
After the last correction, spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, artifact path, direction contract, PRODUCT.md, [document.md](document.md), and write boundary. Without subagents, load [degraded/documenter.md](degraded/documenter.md) and [document.md](document.md) before writing. Verify the outcome: new worlds and approved system changes require token-bearing DESIGN.md **and** `.impeccable/design.json`, not prose alone. Ordinary extensions compare the finished build against the incumbent system, preserve its files, and report the evidence checked; report pre-existing drift without repairing it unasked. Recheck after later edits. Finish only when review and documentation are complete. After the last correction, spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, artifact path, direction contract, PRODUCT.md, [document.md](document.md), and write boundary. Without subagents, load [degraded/documenter.md](degraded/documenter.md) and [document.md](document.md) before writing. Verify the outcome: new worlds and approved system changes require token-bearing DESIGN.md **and** `.impeccable/design.json`, not prose alone. Ordinary extensions compare the finished build against the incumbent system, preserve its files, and report the evidence checked; report pre-existing drift without repairing it unasked. Recheck after later edits. Finish only when review and documentation are complete.
On a comp-led build, record the final review disposition with `.claude/skills/impeccable/scripts/impeccable build-phase finish --disposition <ship|fix|rebuild|recapture>` before the final response. A refused `ship` is an unfinished build; report the outstanding phase with the verdict.
@@ -16,7 +16,7 @@ Reason over the signals; there is no score to obey:
- `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique <surface>` is a strong default. - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique <surface>` is a strong default.
- `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog and closes it when stale or cleared). - `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog and closes it when stale or cleared).
- `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them.
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. **`live` and the bundled `impeccable detect` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with either; the browser overlay and the HTML rule engine don't apply to native app code. - `devServer.running` true → `live` is available for in-browser iteration, and `generate` for one-shot variant runs on a named element; if false, don't lead with either. **`live`, `generate`, and the bundled `impeccable detect` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with any of them; the browser overlay and the HTML rule engine don't apply to native app code.
- Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`. - Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`.
**If `scan.targets` is non-empty and `setup.platform` is not `ios`/`android`/`adaptive`, run `.claude/skills/impeccable/scripts/impeccable detect --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx; it reads HTML/CSS, so skip it for native projects). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it. **If `scan.targets` is non-empty and `setup.platform` is not `ios`/`android`/`adaptive`, run `.claude/skills/impeccable/scripts/impeccable detect --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx; it reads HTML/CSS, so skip it for native projects). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
+1 -1
View File
@@ -1 +1 @@
0.1.6 0.1.5
@@ -19,6 +19,10 @@
"description": "Interactive live variant mode. Select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via HMR. Requires a running dev server. Use when you want to visually experiment with design alternatives in real time.", "description": "Interactive live variant mode. Select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via HMR. Requires a running dev server. Use when you want to visually experiment with design alternatives in real time.",
"argumentHint": "" "argumentHint": ""
}, },
"generate": {
"description": "Agent-driven live variant generation. Boots live mode, finds the named element on the open page, scrolls the browser to it, and delivers N variants in the requested direction for the user to cycle and accept. Use for requests that name an element and a direction, like 'generate 3 bold variants of the pricing cards', skipping manual element picking.",
"argumentHint": "[count] [direction] variants of [element]"
},
"adapt": { "adapt": {
"description": "Adapt designs to work across different screen sizes, devices, contexts, or platforms. Implements breakpoints, fluid layouts, and touch targets. Use when the user mentions responsive design, mobile layouts, breakpoints, viewport adaptation, or cross-device compatibility.", "description": "Adapt designs to work across different screen sizes, devices, contexts, or platforms. Implements breakpoints, fluid layouts, and touch targets. Use when the user mentions responsive design, mobile layouts, breakpoints, viewport adaptation, or cross-device compatibility.",
"argumentHint": "[target] [context (mobile, tablet, print...)]" "argumentHint": "[target] [context (mobile, tablet, print...)]"
@@ -165,6 +165,14 @@
} }
let parameterGenerationState = 'idle'; let parameterGenerationState = 'idle';
let parameterReadyAnnouncedSession = null; let parameterReadyAnnouncedSession = null;
// 'agent' when the generate verb fired this session's Go (the generate
// lane declares no knobs, so its bar never shows a pending Tune chip);
// null for every Go a user presses.
let sessionOrigin = null;
// The generate lane picks for the agent and never edits copy in the
// browser, so its selection carries no edit-copy badge (set on the
// agent-target pick, cleared with the session; a user's pick never sets it).
let editBadgeSuppressed = false;
let svelteComponentSession = null; let svelteComponentSession = null;
let svelteRuntimePromise = null; let svelteRuntimePromise = null;
let pendingSvelteComponentRetryObserver = null; let pendingSvelteComponentRetryObserver = null;
@@ -983,9 +991,20 @@
} }
} catch { /* cross-origin */ } } catch { /* cross-origin */ }
} }
// The selector a mechanical bake would anchor lasting rules on, and how
// many elements it matches right now: the bake refuses anything but one,
// since its rules would restyle every match, not just this element.
const cssIdent = (s) => /^[A-Za-z_-][\w-]*$/.test(s);
const anchorClasses = [...el.classList].filter(cssIdent);
const anchor = el.id && cssIdent(el.id)
? '#' + el.id
: (anchorClasses.length ? el.tagName.toLowerCase() + '.' + anchorClasses.join('.') : null);
let anchorMatches = null;
if (anchor) { try { anchorMatches = document.querySelectorAll(anchor).length; } catch { anchorMatches = null; } }
return { return {
tagName: el.tagName.toLowerCase(), id: el.id || null, tagName: el.tagName.toLowerCase(), id: el.id || null,
classes: [...el.classList], classes: [...el.classList],
anchor, anchorMatches,
textContent: (el.textContent || '').slice(0, 500), textContent: (el.textContent || '').slice(0, 500),
outerHTML: sanitizedContextOuterHTML(el, 10000), outerHTML: sanitizedContextOuterHTML(el, 10000),
computedStyles: { computedStyles: {
@@ -2037,6 +2056,7 @@
function setLiveState(next) { function setLiveState(next) {
state = next; state = next;
window.__IMPECCABLE_LIVE_STATE__ = next; window.__IMPECCABLE_LIVE_STATE__ = next;
retryDeclinedAgentTargets();
syncPageInteractionCursor(); syncPageInteractionCursor();
// Whether a queued steer is still behind a generation is a function of this // Whether a queued steer is still behind a generation is a function of this
// state, so the hint has to move with it, not only with the 5s poll. // state, so the hint has to move with it, not only with the 5s poll.
@@ -4014,6 +4034,7 @@
function hidePendingApplyDock() { function hidePendingApplyDock() {
pendingApplyInFlight = false; pendingApplyInFlight = false;
retryDeclinedAgentTargets();
clearStoredManualApplyState(); clearStoredManualApplyState();
if (pendingIntroAnimation) { pendingIntroAnimation.cancel(); pendingIntroAnimation = null; } if (pendingIntroAnimation) { pendingIntroAnimation.cancel(); pendingIntroAnimation = null; }
if (pendingDockEl) pendingDockEl.style.display = 'none'; if (pendingDockEl) pendingDockEl.style.display = 'none';
@@ -4047,6 +4068,7 @@
function setPendingApplyLoading(loading, count) { function setPendingApplyLoading(loading, count) {
if (!pendingPillEl || !pendingPillLabelEl || !pendingPillCountEl || !pendingTrashBtn) return; if (!pendingPillEl || !pendingPillLabelEl || !pendingPillCountEl || !pendingTrashBtn) return;
pendingApplyInFlight = loading === true; pendingApplyInFlight = loading === true;
if (!pendingApplyInFlight) retryDeclinedAgentTargets();
const currentCount = count || parseInt(pendingPillEl.dataset.count || '0', 10) || 0; const currentCount = count || parseInt(pendingPillEl.dataset.count || '0', 10) || 0;
if (pendingApplyInFlight) storeManualApplyState(currentCount); if (pendingApplyInFlight) storeManualApplyState(currentCount);
else clearStoredManualApplyState(); else clearStoredManualApplyState();
@@ -4688,6 +4710,7 @@
} }
function renderEditBadge(mode) { function renderEditBadge(mode) {
if (editBadgeSuppressed || sessionOrigin === 'agent') mode = 'hidden';
if (mode === 'hidden' || !editBadgeEl) { if (mode === 'hidden' || !editBadgeEl) {
hideConfigureBarTooltip(); hideConfigureBarTooltip();
if (editBadgeEl) editBadgeEl.style.display = 'none'; if (editBadgeEl) editBadgeEl.style.display = 'none';
@@ -6181,6 +6204,8 @@
resetSessionFileMeta(); resetSessionFileMeta();
currentSessionId = null; currentSessionId = null;
parameterGenerationState = 'idle'; parameterGenerationState = 'idle';
sessionOrigin = null;
editBadgeSuppressed = false;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
expectedVariants = 0; expectedVariants = 0;
arrivedVariants = 0; arrivedVariants = 0;
@@ -7112,6 +7137,398 @@
} }
// //
// ------------------------------------------------------------------
// Agent-initiated targeting (the `generate` command). The agent names an
// element by CSS selector over POST /agent-target; the server pushes an
// `agent_target` SSE message here. The overlay resolves the selector,
// scrolls the element into view, enters the same picked state a user
// click produces, and fires the normal Go pipeline, so everything
// downstream (generate event, variants, cycling, accept) is unchanged.
// The verdict goes back through POST /agent-target-result, which resolves
// the agent's held-open CLI call.
function postAgentTargetResult(targetId, result) {
fetch('http://localhost:' + PORT + '/agent-target-result?token=' + TOKEN, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: TOKEN, targetId, clientId: AGENT_TARGET_CLIENT_ID, ...result }),
}).catch(() => { /* server gone; nothing to report to */ });
}
function describeAgentTargetCandidate(el) {
return {
tag: el.tagName.toLowerCase(),
id: el.id || null,
classes: [...el.classList].filter((c) => !c.startsWith('impeccable-')),
text: (el.textContent || '').trim().slice(0, 80),
};
}
function resolveAgentTargetElement(msg) {
let matched;
try {
matched = [...document.querySelectorAll(msg.selector)];
} catch {
return { error: { ok: false, error: 'invalid_selector', selector: msg.selector } };
}
let candidates = matched.filter((el) => pickable(el));
if (msg.text) {
const needle = String(msg.text).toLowerCase();
candidates = candidates.filter((el) => (el.textContent || '').toLowerCase().includes(needle));
}
if (candidates.length === 0) {
return {
error: {
ok: false,
error: 'no_match',
selector: msg.selector,
matchCount: 0,
// How many nodes the raw selector hit before the pickable/text
// filters: distinguishes a wrong selector from an unpickable match.
rawMatchCount: matched.length,
},
};
}
if (Number.isInteger(msg.index)) {
const el = candidates[msg.index - 1];
if (!el) {
return { error: { ok: false, error: 'index_out_of_range', selector: msg.selector, matchCount: candidates.length } };
}
return { el, matchCount: candidates.length };
}
if (candidates.length > 1) {
return {
error: {
ok: false,
error: 'ambiguous',
selector: msg.selector,
matchCount: candidates.length,
candidates: candidates.slice(0, 8).map(describeAgentTargetCandidate),
},
};
}
return { el: candidates[0], matchCount: 1 };
}
function scrollAgentTargetIntoView(el, done) {
const rect = el.getBoundingClientRect();
if (rect.top >= 0 && rect.bottom <= window.innerHeight) { done(); return; }
let settled = false;
let fallback = null;
const finish = () => {
if (settled) return;
settled = true;
removeEventListener('scrollend', finish, true);
if (fallback) clearTimeout(fallback);
done();
};
// scrollend where supported; a timer covers engines without it and the
// no-movement case (element already at its final resting position).
addEventListener('scrollend', finish, true);
fallback = setTimeout(finish, 1200);
el.scrollIntoView({ block: 'center', behavior: 'smooth' });
}
// One id per page load: the server keys claims and roll-call reports on
// it, and only the tab that holds the lease can renew it.
const AGENT_TARGET_CLIENT_ID = id8();
// The agent target an agent-initiated Go is serving: set by
// actOnAgentTarget around its handleGo call, read once by handleGo.
let agentTargetForGo = null;
// The helper's word on its global bar. The generate lane asks the helper
// to keep it out of the way (`impeccable live --no-live-bar`, or an agent
// target carrying hideLiveBar), and the helper tells every connected tab
// at once (`live_bar`) and every later connection on `connected`, so the
// bar stays hidden in every tab, through reloads, the accept, and the
// bake, until the helper stops and takes the overlay with it. The variant
// controls still show.
let liveBarHiddenByHelper = false;
function applyLiveBarPreference(hidden) {
liveBarHiddenByHelper = hidden === true;
setLiveBarHidden(liveBarHiddenByHelper);
}
// A plain live session must never notice this code: hiding remembers the
// bar's own display value and restoring puts exactly that back, and a
// restore on a bar that is not hidden is a no-op, so the `connected`
// frame every session receives changes nothing unless the lane asked.
function setLiveBarHidden(hidden) {
if (!globalBarEl) return;
if (hidden) {
if (globalBarEl.style.display !== 'none') {
globalBarEl.dataset.liveBarDisplay = globalBarEl.style.display || 'flex';
globalBarEl.style.display = 'none';
}
return;
}
if (globalBarEl.style.display === 'none') {
globalBarEl.style.display = globalBarEl.dataset.liveBarDisplay || 'flex';
}
}
function claimAgentTarget(targetId, report) {
return fetch('http://localhost:' + PORT + '/agent-target-claim?token=' + TOKEN, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: TOKEN, targetId, clientId: AGENT_TARGET_CLIENT_ID, ...report }),
}).then((res) => res.json())
.then((j) => ({ granted: !!j && j.granted === true, pending: !!j && j.pending === true }))
.catch(() => ({ granted: false, pending: false }));
}
// `exceptTargetId` is the target this call is about: a tab acting on it
// is not busy for itself, but it is busy for every other target, or two
// held requests could both be claimed here and the second Go would
// overwrite the session the first one minted.
function agentTargetBusyReason(exceptTargetId) {
if (pendingApplyInFlight) return 'manual_apply_in_flight';
if (state !== 'IDLE' && state !== 'PICKING' && state !== 'CONFIGURING') return 'session_active';
for (const [targetId, status] of agentTargetsSeen) {
if (status === 'acting' && targetId !== exceptTargetId) return 'agent_target_in_flight';
}
return null;
}
// Targets this tab declined as busy. A busy report is only this tab's word
// at that moment: the moment it is free again (setLiveState), it claims
// each of these as eligible, and the server drops the stale report, so a
// busy verdict is never built on a tab that has since gone idle. The
// server denies claims for resolved targets, so retries are harmless.
const busyDeclinedTargets = new Map();
function declineAgentTargetBusy(msg, busy) {
busyDeclinedTargets.set(msg.targetId, msg);
noteAgentTarget(msg.targetId, 'declined');
claimAgentTarget(msg.targetId, { eligible: false, state, reason: busy });
}
// A torn-down overlay, or one whose helper connection is gone, cannot
// serve a target and must not even claim one: it would hold the lease for
// a request it will never act on.
function agentTargetOverlayGone() {
return !evtSource;
}
// A denied claimant retries at this cadence, a little over the lease, so
// the first retry after a dead holder's lease lapses is granted.
const AGENT_TARGET_RESCUE_RETRY_MS = 3500;
// Claim the lease and act as the holder. A denied claim means another tab
// holds the lease. That holder can die before posting its result (reload,
// crash, even after renewing), and its lease lapses after ~3s, so this tab
// keeps retrying for as long as the server still holds the request: the
// answer's `pending` is the server's word that the request is alive, and
// it turns false the moment the request resolved or timed out, so no tab
// retries a request nobody awaits. A tab that turned busy meanwhile joins
// the roll call instead of taking a lease it cannot use. The first claim
// and the busy-to-idle re-claim share this.
function claimAndActOnAgentTarget(msg) {
if (agentTargetOverlayGone()) return;
const busy = agentTargetBusyReason(msg.targetId);
if (busy) { declineAgentTargetBusy(msg, busy); return; }
if (declineAgentTargetUnresolvable(msg)) return;
claimAgentTarget(msg.targetId, { eligible: true }).then((claim) => {
if (claim.granted) { noteAgentTarget(msg.targetId, 'acting'); actOnAgentTarget(msg); return; }
noteAgentTarget(msg.targetId, 'denied');
if (!claim.pending) return;
setTimeout(() => claimAndActOnAgentTarget(msg), AGENT_TARGET_RESCUE_RETRY_MS);
});
}
function retryDeclinedAgentTargets() {
if (busyDeclinedTargets.size === 0 || agentTargetBusyReason()) return;
for (const [targetId, msg] of busyDeclinedTargets) {
busyDeclinedTargets.delete(targetId);
claimAndActOnAgentTarget(msg);
}
}
// This page's participation in each target it heard: 'acting' once a
// claim was granted, 'done' once it replied (or stood down from a lapsed
// lease), else the word it last gave. The server replays pending targets
// to every connection that opens. After a reconnect that overlapped the
// old connection the server still holds this page's word; after one that
// did not, it dropped the word on the close, so a replayed target is
// handled again: a busy or unresolvable page re-declines (idempotent), an
// idle page claims.
const agentTargetsSeen = new Map();
function noteAgentTarget(targetId, status) {
agentTargetsSeen.set(targetId, status);
if (agentTargetsSeen.size > 100) agentTargetsSeen.delete(agentTargetsSeen.keys().next().value);
}
// A target this page took a lease on is off-limits for a replay: while
// acting (a second claim or Go), and once done, because its result may
// still be on the wire and this tab is GENERATING by then, so handling
// the replay would decline busy, hand the lease back mid-resolution, and
// let another tab fire a second Go.
function agentTargetTaken(targetId) {
const status = agentTargetsSeen.get(targetId);
return status === 'acting' || status === 'done';
}
// Only a page that can resolve the target claims it. A tab whose page
// lacks the element declines with its resolution verdict instead, so a
// first-wins claim never lets the wrong page answer for a target that
// another page has. The server prefers a busy report (a tab that could
// serve later) over these, and returns the resolution verdict only when
// no connected page can serve.
//
// An element can be momentarily absent (a route still rendering, an HMR
// commit mid-swap), so a failed resolution is not this page's final word:
// it is re-checked a few times over about two seconds, claiming the
// moment the element mounts, and only the last miss is reported. The
// server's timeout still bounds the whole exchange.
// The page reports the miss at once (so the other overlays' words can
// complete the roll call) and keeps re-checking at this cadence for as
// long as the server says the request is pending: the server holds an
// all-no_match roll call open for a short grace precisely so a late mount
// can still be claimed, drops the stale report on an eligible claim, and
// ends the watch by answering pending:false once the request resolved or
// timed out.
const AGENT_TARGET_RESOLVE_WATCH_MS = 500;
function declineAgentTargetUnresolvable(msg) {
const probe = resolveAgentTargetElement(msg);
if (!probe.error) return false;
reportAgentTargetUnresolvable(msg, probe.error);
return true;
}
function reportAgentTargetUnresolvable(msg, error) {
noteAgentTarget(msg.targetId, 'declined');
claimAgentTarget(msg.targetId, { eligible: false, state, reason: 'no_match', result: error }).then((answer) => {
if (!answer.pending) return;
setTimeout(() => watchAgentTargetResolution(msg, error), AGENT_TARGET_RESOLVE_WATCH_MS);
});
}
function watchAgentTargetResolution(msg, lastError) {
if (agentTargetOverlayGone() || agentTargetTaken(msg.targetId)) return;
const busy = agentTargetBusyReason(msg.targetId);
if (busy) { declineAgentTargetBusy(msg, busy); return; }
const probe = resolveAgentTargetElement(msg);
if (!probe.error) { claimAndActOnAgentTarget(msg); return; }
// Still unresolvable: re-report (idempotent); the answer says whether
// the server is still holding the request open.
reportAgentTargetUnresolvable(msg, probe.error || lastError);
}
function handleAgentTarget(msg) {
if (!msg || typeof msg.targetId !== 'string') return;
if (agentTargetTaken(msg.targetId)) return;
noteAgentTarget(msg.targetId, 'heard');
const busy = agentTargetBusyReason(msg.targetId);
if (busy) {
// Roll call: a busy tab reports itself and never acts. The server
// answers `busy` the moment every connected overlay has reported, so
// an idle tab elsewhere is never raced by a timer.
declineAgentTargetBusy(msg, busy);
return;
}
if (declineAgentTargetUnresolvable(msg)) return;
// Eligible tabs race for the server's lease and only the holder acts. A
// hidden tab yields a short head start so a visible one wins when both
// exist, and still serves the request on its own: the user finds the
// selection waiting when they return to it.
setTimeout(() => claimAndActOnAgentTarget(msg), document.hidden ? 150 : 0);
}
function actOnAgentTarget(msg) {
if (agentTargetOverlayGone()) return;
// Every exit ends this tab's acting state, so a later target is not
// refused for a Go that already happened or never will.
const reply = (result) => { noteAgentTarget(msg.targetId, 'done'); postAgentTargetResult(msg.targetId, result); };
const busy = agentTargetBusyReason(msg.targetId);
if (busy) {
// Turned busy between claim and act: report it, which also hands the
// lease back so the roll call can complete or a rescuer can claim.
declineAgentTargetBusy(msg, busy);
return;
}
const resolved = resolveAgentTargetElement(msg);
if (resolved.error) {
// The element went away between claim and act. A result would end the
// request for every tab; a decline hands the lease back so another
// page or a remount can still serve it.
reportAgentTargetUnresolvable(msg, resolved.error);
return;
}
const el = resolved.el;
if (msg.dryRun) {
reply({
ok: true,
dryRun: true,
matchCount: resolved.matchCount,
element: describeAgentTargetCandidate(el),
});
return;
}
scrollAgentTargetIntoView(el, () => {
// Torn down during the scroll settle: do not renew. The lease lapses
// for a rescuer instead of Go minting a session on a dismantled
// overlay.
if (agentTargetOverlayGone()) return;
// Renew the lease right before the irreversible part: a tab whose
// lease lapsed while it scrolled (a rescuer took over) stops here, so
// one request never gets two Go presses.
claimAgentTarget(msg.targetId, { eligible: true }).then((renewal) => {
if (!renewal.granted) { noteAgentTarget(msg.targetId, 'done'); return; }
// An insert placement left mid-configure gives way, exactly as a
// click outside it does in handleClick.
if (state === 'CONFIGURING' && configureKind === 'insert') cancelInsertConfigure();
// Mirror of the user-click pick entry in handleClick, minus the
// pick-mode gate (the agent's intent replaces the toggle); the entry
// goes through beginNewLiveConfiguration like every other pick so
// deferred recovery sees a fresh interaction revision.
selectedElement = el;
beginNewLiveConfiguration();
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
showBar('configure');
editBadgeSuppressed = true;
renderEditBadge('hidden');
startScrollTracking();
maybePrefetchPage();
maybeWarnConditionalAncestor(selectedElement);
// Preset what the agent asked for, then fire the same Go a user press
// fires. handleGo reads exactly these inputs.
selectedAction = msg.action;
selectedCount = msg.count;
// updateBarContent rebuilds the configure row and replaces the input
// element, so the prompt must be written into the input it creates,
// never before (the action-chip click handler does the same dance).
updateBarContent('configure');
const input = uiGetById(PREFIX + '-input');
if (input) input.value = msg.prompt || '';
// The target rides on the generate event too: the helper resolves
// the request from whichever lands first, so a page that dies
// between Go and its result cannot leave the request pending for a
// second Go elsewhere.
const candidate = describeAgentTargetCandidate(el);
agentTargetForGo = { targetId: msg.targetId, matchCount: resolved.matchCount, action: msg.action, count: msg.count, element: candidate };
handleGo();
agentTargetForGo = null;
if (state === 'GENERATING' && currentSessionId) {
reply({
ok: true,
matchCount: resolved.matchCount,
sessionId: currentSessionId,
action: msg.action,
count: msg.count,
element: candidate,
});
} else {
reply({ ok: false, error: 'go_failed', state });
}
});
});
}
// SSE (server→browser) + fetch POST (browser→server) // SSE (server→browser) + fetch POST (browser→server)
// Zero-dependency replacement for WebSocket. // Zero-dependency replacement for WebSocket.
// //
@@ -7121,7 +7538,7 @@
const SSE_MAX_RETRIES = 20; // generous: heartbeats keep the connection alive, so retries mean real trouble const SSE_MAX_RETRIES = 20; // generous: heartbeats keep the connection alive, so retries mean real trouble
function connectSSE() { function connectSSE() {
evtSource = new EventSource('http://localhost:' + PORT + '/events?token=' + TOKEN); evtSource = new EventSource('http://localhost:' + PORT + '/events?token=' + TOKEN + '&clientId=' + AGENT_TARGET_CLIENT_ID);
evtSource.onopen = () => { evtSource.onopen = () => {
sseRetries = 0; // reset on successful (re)connect sseRetries = 0; // reset on successful (re)connect
@@ -7132,8 +7549,11 @@
let msg; try { msg = JSON.parse(e.data); } catch { return; } let msg; try { msg = JSON.parse(e.data); } catch { return; }
switch (msg.type) { switch (msg.type) {
case 'connected': case 'connected':
applyLiveBarPreference(msg.hideLiveBar === true);
hasProjectContext = !!msg.hasProjectContext; hasProjectContext = !!msg.hasProjectContext;
if (!hasProjectContext) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000); // The generate lane runs without PRODUCT.md by design and never
// sends the user to init, so its quiet chrome skips this notice.
if (!hasProjectContext && !liveBarHiddenByHelper) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000);
console.log('[impeccable] Live mode connected.'); console.log('[impeccable] Live mode connected.');
syncAgentPollingUi(!!msg.agentPolling); syncAgentPollingUi(!!msg.agentPolling);
startAgentStatusPoll(); startAgentStatusPoll();
@@ -7143,9 +7563,15 @@
syncPageInteractionCursor(); syncPageInteractionCursor();
syncPageChatFocus('sse-connected'); syncPageChatFocus('sse-connected');
break; break;
case 'live_bar':
applyLiveBarPreference(msg.hidden === true);
break;
case 'agent_polling': case 'agent_polling':
syncAgentPollingUi(!!msg.connected); syncAgentPollingUi(!!msg.connected);
break; break;
case 'agent_target':
handleAgentTarget(msg);
break;
case 'agent_phase': case 'agent_phase':
if (msg.id === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) { if (msg.id === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) {
// Advance the visible phase monotonically. A behind/resumed // Advance the visible phase monotonically. A behind/resumed
@@ -7208,6 +7634,11 @@
disableInlineEdit(); disableInlineEdit();
refreshParamsPanel(); refreshParamsPanel();
} }
// The done reply is the agent's last word on this generation:
// with every variant mounted and no knobs declared, the Tune
// chip must stop spinning. A reload between the mount and this
// reply restored the pending state from the cache.
completeParameterGenerationIfReady();
break; break;
} }
// Source fallback when HMR did not land variants in this tab. // Source fallback when HMR did not land variants in this tab.
@@ -7371,6 +7802,15 @@
}).then(async res => { }).then(async res => {
if (res.ok) return res; if (res.ok) return res;
const body = await res.json().catch(() => ({})); const body = await res.json().catch(() => ({}));
// The helper refused to open a session for an agent target it has
// already answered (another page served it after this page's lease
// lapsed mid-capture, or the request timed out): drop the local
// session and hand the surface back.
if (body.error === 'agent_target_already_served' && msg.type === 'generate'
&& msg.id && msg.id === currentSessionId) {
abandonSupersededGo(msg.id);
return null;
}
// The server refused to journal progress for a session it has never // The server refused to journal progress for a session it has never
// seen: this browser is carrying state from another project or a // seen: this browser is carrying state from another project or a
// wiped store (two apps sharing a localhost port). Continuing to // wiped store (two apps sharing a localhost port). Continuing to
@@ -7392,6 +7832,14 @@
return sessionCreationGate.then(doSend); return sessionCreationGate.then(doSend);
} }
function abandonSupersededGo(sessionId) {
if (sessionId !== currentSessionId) return;
console.warn('[impeccable] The helper already answered this agent target; clearing session ' + sessionId + '.');
markSessionHandled();
cleanup({ instantChrome: true });
showToast('The helper already answered this request, so this session was cleared. Pick an element to start fresh.', 6000);
}
let abandonedForeignSessionId = null; let abandonedForeignSessionId = null;
function abandonForeignSession(sessionId) { function abandonForeignSession(sessionId) {
if (abandonedForeignSessionId === sessionId || sessionId !== currentSessionId) return; if (abandonedForeignSessionId === sessionId || sessionId !== currentSessionId) return;
@@ -7796,6 +8244,7 @@
visibleVariant = 0; visibleVariant = 0;
generationPhase = 'queued'; generationPhase = 'queued';
parameterGenerationState = 'pending'; parameterGenerationState = 'pending';
sessionOrigin = agentTargetForGo ? 'agent' : null;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
resetSessionFileMeta(); resetSessionFileMeta();
@@ -7821,6 +8270,24 @@
}; };
if (snapshot.comments.length > 0) basePayload.comments = snapshot.comments; if (snapshot.comments.length > 0) basePayload.comments = snapshot.comments;
if (snapshot.strokes.length > 0) basePayload.strokes = snapshot.strokes; if (snapshot.strokes.length > 0) basePayload.strokes = snapshot.strokes;
if (agentTargetForGo) {
// An agent-initiated Go names the target it serves (see
// actOnAgentTarget): the helper resolves that request from this event
// as well as from the overlay's own result post.
basePayload.agentTarget = {
targetId: agentTargetForGo.targetId,
clientId: AGENT_TARGET_CLIENT_ID,
result: {
ok: true,
matchCount: agentTargetForGo.matchCount,
sessionId: currentSessionId,
action: agentTargetForGo.action,
count: agentTargetForGo.count,
element: agentTargetForGo.element,
},
};
agentTargetForGo = null;
}
// Hide the interactive overlay so it doesn't linger during generation. // Hide the interactive overlay so it doesn't linger during generation.
hideAnnotOverlay(); hideAnnotOverlay();
@@ -7881,6 +8348,7 @@
visibleVariant = 0; visibleVariant = 0;
generationPhase = 'queued'; generationPhase = 'queued';
parameterGenerationState = 'pending'; parameterGenerationState = 'pending';
sessionOrigin = agentTargetForGo ? 'agent' : null;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
resetSessionFileMeta(); resetSessionFileMeta();
selectedElement = placeholderElement; selectedElement = placeholderElement;
@@ -8927,6 +9395,8 @@ void main() {
pagePickSkipClick = false; pagePickSkipClick = false;
currentSessionId = null; currentSessionId = null;
parameterGenerationState = 'idle'; parameterGenerationState = 'idle';
sessionOrigin = null;
editBadgeSuppressed = false;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
selectedAction = 'impeccable'; selectedAction = 'impeccable';
pendingAcceptedSession = null; pendingAcceptedSession = null;
@@ -9018,6 +9488,7 @@ void main() {
paramsCurrentValues = { ...saved.paramValues }; paramsCurrentValues = { ...saved.paramValues };
} }
if (saved.parameterState) parameterGenerationState = saved.parameterState; if (saved.parameterState) parameterGenerationState = saved.parameterState;
sessionOrigin = saved.origin === 'agent' ? 'agent' : null;
if (saved.generationPhase) generationPhase = saved.generationPhase; if (saved.generationPhase) generationPhase = saved.generationPhase;
} }
@@ -9105,7 +9576,12 @@ void main() {
} }
function restoreSessionWithoutWrapper(reason, activeSessions) { function restoreSessionWithoutWrapper(reason, activeSessions) {
const cached = loadSession(); // The session cache is per origin, so a tab on another page of the same
// app sees this page's session too. Only the page that saved it may
// resume it: the server-adoption branch below already applies the same
// check, and a tab on another page has nothing to render for it.
const cachedRaw = loadSession();
const cached = cachedRaw?.id && !pageMatchesCurrent(cachedRaw.pageUrl) ? null : cachedRaw;
// localStorage is a cache, not a gate. A cleared tab, a second browser // localStorage is a cache, not a gate. A cleared tab, a second browser
// profile, or a teardown that dropped local state all leave the durable // profile, or a teardown that dropped local state all leave the durable
// server session as the only record of work in progress; adopt it instead // server session as the only record of work in progress; adopt it instead
@@ -9218,6 +9694,7 @@ void main() {
pageUrl: location.pathname, pageUrl: location.pathname,
paramValues: { ...paramsCurrentValues }, paramValues: { ...paramsCurrentValues },
parameterState: parameterGenerationState, parameterState: parameterGenerationState,
origin: sessionOrigin || undefined,
insertPlaceholder: insertPlaceholderSnapshot || undefined, insertPlaceholder: insertPlaceholderSnapshot || undefined,
pickedAnchor: pickedAnchorSnapshot || undefined, pickedAnchor: pickedAnchorSnapshot || undefined,
pickedAnchorViewportTop: Number.isFinite(pickedAnchorViewportTop) ? pickedAnchorViewportTop : undefined, pickedAnchorViewportTop: Number.isFinite(pickedAnchorViewportTop) ? pickedAnchorViewportTop : undefined,
@@ -9343,6 +9820,8 @@ void main() {
pagePickSkipClick = false; pagePickSkipClick = false;
currentSessionId = null; currentSessionId = null;
parameterGenerationState = 'idle'; parameterGenerationState = 'idle';
sessionOrigin = null;
editBadgeSuppressed = false;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
selectedAction = 'impeccable'; selectedAction = 'impeccable';
renderEditBadge('hidden'); renderEditBadge('hidden');
@@ -9601,6 +10080,14 @@ void main() {
const resumedState = arrivedVariants > 0 ? 'CYCLING' : 'GENERATING'; const resumedState = arrivedVariants > 0 ? 'CYCLING' : 'GENERATING';
// A reload between the variants mounting and the agent's done reply
// restores a pending Tune state from the cache; the helper knows whether
// that generation already finished.
if (arrivedVariants >= expectedVariants && expectedVariants > 0
&& (parameterGenerationState === 'pending' || parameterGenerationState === 'loading')) {
settleParameterStateFromHelper(sessionId);
}
// Find the visible variant's content element for highlight positioning. // Find the visible variant's content element for highlight positioning.
const isInsert = wrapper.dataset.impeccableMode === 'insert'; const isInsert = wrapper.dataset.impeccableMode === 'insert';
const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null; const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null;
@@ -11065,6 +11552,21 @@ void main() {
} }
} }
// After a resume the cache may say the Tune knobs are still coming while
// the agent already replied done before the reload. The helper's session
// record settles it; otherwise the done reply on SSE does.
function settleParameterStateFromHelper(sessionId) {
fetch('http://localhost:' + PORT + '/status?token=' + TOKEN, { cache: 'no-store' })
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
if (!data || sessionId !== currentSessionId) return;
const session = (data.activeSessions || []).find((s) => s && s.id === sessionId);
if (!session) return;
if (session.generationCompletedAt || session.generationPhase === 'completed') completeParameterGenerationIfReady();
})
.catch(() => { /* the done reply on SSE settles it otherwise */ });
}
function fetchAgentPollingStatus() { function fetchAgentPollingStatus() {
fetch('http://localhost:' + PORT + '/status?token=' + TOKEN, { cache: 'no-store' }) fetch('http://localhost:' + PORT + '/status?token=' + TOKEN, { cache: 'no-store' })
.then((res) => (res.ok ? res.json() : null)) .then((res) => (res.ok ? res.json() : null))
@@ -11104,11 +11606,15 @@ void main() {
uiAppendStyle(s); uiAppendStyle(s);
} }
// The generate lane's helper says so in the served script itself, so a
// lane session never draws the bar at all; every other session mounts
// it exactly as before.
const barHiddenFromStart = window.__IMPECCABLE_LIVE_BAR_HIDDEN__ === true;
globalBarEl = el('div', { globalBarEl = el('div', {
position: 'fixed', bottom: '14px', left: '50%', position: 'fixed', bottom: '14px', left: '50%',
transform: 'translateX(-50%) translateY(20px)', transform: 'translateX(-50%) translateY(20px)',
zIndex: Z.bar + 5, zIndex: Z.bar + 5,
display: 'flex', alignItems: 'stretch', display: barHiddenFromStart ? 'none' : 'flex', alignItems: 'stretch',
gap: '0', gap: '0',
width: 'max-content', width: 'max-content',
background: P.surface, background: P.surface,
@@ -11124,6 +11630,10 @@ void main() {
}); });
globalBarEl.id = PREFIX + '-global-bar'; globalBarEl.id = PREFIX + '-global-bar';
globalBarEl.dataset.theme = theme; globalBarEl.dataset.theme = theme;
if (barHiddenFromStart) {
liveBarHiddenByHelper = true;
globalBarEl.dataset.liveBarDisplay = 'flex';
}
// Brand mark - kinpaku Impeccable icon (site header / favicon paths). // Brand mark - kinpaku Impeccable icon (site header / favicon paths).
const brand = el('span', { const brand = el('span', {
@@ -11519,6 +12029,9 @@ void main() {
// Listen for detection results AND ready signal // Listen for detection results AND ready signal
window.addEventListener('message', onDetectMessage); window.addEventListener('message', onDetectMessage);
updateGlobalBarState(); updateGlobalBarState();
// The helper may already have said the bar stays hidden (a connect
// that raced the bar build, or a reload mid-lane): re-apply it here.
if (liveBarHiddenByHelper) setLiveBarHidden(true);
} }
function updateGlobalBarState() { function updateGlobalBarState() {
@@ -11715,6 +12228,13 @@ void main() {
/** Full teardown: remove all UI, disconnect SSE, clean up. */ /** Full teardown: remove all UI, disconnect SSE, clean up. */
function teardown() { function teardown() {
// Declined targets die with the overlay: the IDLE transition below must
// not re-claim a lease this page can no longer act on. So does the
// target ledger: an 'acting' entry from a Go that never happened must
// not refuse every target the next connection hears.
busyDeclinedTargets.clear();
agentTargetsSeen.clear();
liveBarHiddenByHelper = false;
stopAgentStatusPoll(); stopAgentStatusPoll();
hideAgentPollTooltip(); hideAgentPollTooltip();
if (agentPollTooltipEl) { if (agentPollTooltipEl) {
@@ -16,10 +16,6 @@ Do not redesign. Preserve the reference's visual role, silhouette, palette, ligh
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 `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; 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; report a card too thin to brief a comp, never pad it 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 (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 keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. 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. 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 `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; 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; report a card too thin to brief a comp, never pad it 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 (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 keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. 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.
## Review handoff
After the initial production batch, return the actual files and any unresolved drift to the parent for the user-facing component review in [component-review.md](../reference/component-review.md). The parent includes rendered code regions alongside these rasters. A parent or automatic visual check is not a substitute for that human checkpoint. Apply requested repairs and preserve unchanged assets; do not self-approve them. This checkpoint does not apply to the Decision Comps job above.
## Input Contract ## Input Contract
Expect the measured spec (`.impeccable/build/spec.json`, written by `impeccable comp-spec` from the approved comp), the approved comp path, and the skill scripts path. Optionally: a subset of region ids to produce, extra prompt notes per region, and format or transparency needs. Everything else you need is in the spec: each raster region's id, kind (plate, image, texture), pixel box, sampled palette, aspect, note, and the plate path it must land on. Expect the measured spec (`.impeccable/build/spec.json`, written by `impeccable comp-spec` from the approved comp), the approved comp path, and the skill scripts path. Optionally: a subset of region ids to produce, extra prompt notes per region, and format or transparency needs. Everything else you need is in the spec: each raster region's id, kind (plate, image, texture), pixel box, sampled palette, aspect, note, and the plate path it must land on.
+1 -1
View File
@@ -20,7 +20,7 @@ Expect: the original request; the confirmed user answers; the artifact path(s);
## Checks, in order ## Checks, in order
0. **Evidence.** Before any other check, verify the required captures exist and every capture is valid. Required: the platform's full viewport set (web: `desktop.png` and `mobile.png`; native: one capture per shipped device class), plus every capture the calling brief names as required, a reported user viewport (`user-<width>.png`) included. Valid: no black or blank regions, content matching what the filename claims (a visit capture showing the About section is invalid), the document top visible where the file claims a full page, dimensions that make sense for the named viewport. A required capture that is absent fails exactly like one that is malformed: a viewport nobody captured is a viewport nobody inspected, and it cannot ship. When any capture fails, the whole review changes shape: return `disposition: recapture` as the first line, then one section, `recapture`, listing each missing or invalid file and what a valid capture of it shows, and stop. Never build a matrix on malformed evidence; a verdict derived from a broken capture launders the breakage into an approval, and the parent owes you a full re-review on valid captures, not a scoring round. 0. **Evidence.** Before any other check, verify the required captures exist and every capture is valid. Required: the platform's full viewport set (web: `desktop.png` and `mobile.png`; native: one capture per shipped device class), plus every capture the calling brief names as required, a reported user viewport (`user-<width>.png`) included. Valid: no black or blank regions, content matching what the filename claims (a visit capture showing the About section is invalid), the document top visible where the file claims a full page, dimensions that make sense for the named viewport. A required capture that is absent fails exactly like one that is malformed: a viewport nobody captured is a viewport nobody inspected, and it cannot ship. When any capture fails, the whole review changes shape: return `disposition: recapture` as the first line, then one section, `recapture`, listing each missing or invalid file and what a valid capture of it shows, and stop. Never build a matrix on malformed evidence; a verdict derived from a broken capture launders the breakage into an approval, and the parent owes you a full re-review on valid captures, not a scoring round.
1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and every phase before `review` is `closed` or explicitly `skipped`; an open or failed phase is a material finding. A comp-led config with no state file, or a state whose `comps` phase is neither `closed` nor `skipped`, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. 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, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all. 1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and its `comps` (or `skipped` when a surface round locked the comp), `spec`, `plates`, and `hero` phases are `closed`; a comp-led config with no state file, or a state whose `comps` phase never closed, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. 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, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all.
2. **Fidelity.** Start from the measurement, then judge what it cannot: read `.impeccable/review/diff/final/report.json` (and hero) first; every region scored `missing` or `contradicted` is a matrix row in that state unless the paired crop under `regions/` shows the score is wrong, and you say why; a region scored `match` still gets your eye for lettering character and material, which the numbers do not measure. Then, 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; 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. Three 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; medium is part of the promise. GROUND: the page field's value and temperature against the comp's, sampled from pixels on both sides when tooling allows rather than judged from memory, and read as the net on-screen result where a texture or tile paints over the base color; a ground warmer or cooler than the comp's is contradicted however faithfully the layout matches, and drift toward the rendition prior (warm cream on light grounds, blue-black slate on dark) is the direction to hunt. With no approved comp, 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 renders) as contradicted on its face; imitation material is the single most reliable mark of machine-made design. GROUND narrows rather than lapses: with no comp to sample, a color OWN-WORLD names is the target and the same warmer-or-cooler judgment applies; when OWN-WORLD names none, there is no GROUND authority, and the review says so in place of a verdict, because a target the reviewer invents turns the check into taste. A critique-reference comp on such a build is provocation, not spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is what the image dared that the build did not, and 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. A fix that requires producing an asset says so explicitly ("produce: <region> 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. 2. **Fidelity.** Start from the measurement, then judge what it cannot: read `.impeccable/review/diff/final/report.json` (and hero) first; every region scored `missing` or `contradicted` is a matrix row in that state unless the paired crop under `regions/` shows the score is wrong, and you say why; a region scored `match` still gets your eye for lettering character and material, which the numbers do not measure. Then, 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; 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. Three 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; medium is part of the promise. GROUND: the page field's value and temperature against the comp's, sampled from pixels on both sides when tooling allows rather than judged from memory, and read as the net on-screen result where a texture or tile paints over the base color; a ground warmer or cooler than the comp's is contradicted however faithfully the layout matches, and drift toward the rendition prior (warm cream on light grounds, blue-black slate on dark) is the direction to hunt. With no approved comp, 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 renders) as contradicted on its face; imitation material is the single most reliable mark of machine-made design. GROUND narrows rather than lapses: with no comp to sample, a color OWN-WORLD names is the target and the same warmer-or-cooler judgment applies; when OWN-WORLD names none, there is no GROUND authority, and the review says so in place of a verdict, because a target the reviewer invents turns the check into taste. A critique-reference comp on such a build is provocation, not spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is what the image dared that the build did not, and 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. A fix that requires producing an asset says so explicitly ("produce: <region> 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. 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, 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. 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, 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.
+3 -2
View File
@@ -1,7 +1,7 @@
--- ---
name: impeccable name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks. description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.4.0 version: 4.3.1
license: Apache 2.0 license: Apache 2.0
--- ---
@@ -63,7 +63,8 @@ Choose the mode from the requested surface, not the product, and persist it only
| `clarify [target]` | Fix | Improve UX copy, labels, and error messages | [reference/clarify.md](reference/clarify.md) | | `clarify [target]` | Fix | Improve UX copy, labels, and error messages | [reference/clarify.md](reference/clarify.md) |
| `adapt [target]` | Fix | Adapt for different devices and screen sizes | [reference/adapt.md](reference/adapt.md) · native: [reference/adapt.native.md](reference/adapt.native.md) | | `adapt [target]` | Fix | Adapt for different devices and screen sizes | [reference/adapt.md](reference/adapt.md) · native: [reference/adapt.native.md](reference/adapt.native.md) |
| `optimize [target]` | Fix | Diagnose and fix UI performance | [reference/optimize.md](reference/optimize.md) | | `optimize [target]` | Fix | Diagnose and fix UI performance | [reference/optimize.md](reference/optimize.md) |
| `live` | Iterate | Visual variant mode: pick elements in the browser, generate alternatives | [reference/live.md](reference/live.md) | | `live` | Iterate | Visual variant mode: pick elements in the browser, iterate on alternatives | [reference/live.md](reference/live.md) |
| `generate [n] [action] [element]` | Iterate | Variants, versions, or alternatives of a named element to choose from in the live browser; no manual picking | [reference/generate.md](reference/generate.md) |
Routing: Routing:
@@ -188,6 +188,12 @@ Test thoroughly across contexts:
- **Edge cases**: Very small screens (320px), very large screens (4K) - **Edge cases**: Very small screens (320px), very large screens (4K)
- **Slow connections**: Test on throttled network - **Slow connections**: Test on throttled network
**Custom controls** (sliders, drag surfaces, scrollable control strips): a before/after slider can pass every width check above and still refuse to drag on iOS, so exercise each one in scope in the same batched round as the checks above:
- **Primary gesture**: Tap it and confirm it responds as designed, then drag it with the target input method; the drag must complete, not just start
- **Scroll across it**: A swipe along the page's scroll axis across the control scrolls the page or container without activating it; a drag that starts on the control along its axis moves the control, not the page. Neither failure throws an error, so try both
- **Evidence**: Say what produced the evidence: an emulated viewport, synthesized touch input through a browser tool, which engine ran it (Chromium is not Safari), or a physical device. Screenshots and resized viewports verify layout, never a gesture. Name what stayed untested and move on; unreachable hardware is a reported gap, not a blocker
When the adaptation feels native to each context, hand off to `/impeccable polish` for the final pass. When the adaptation feels native to each context, hand off to `/impeccable polish` for the final pass.
--- ---
+2 -1
View File
@@ -48,11 +48,12 @@ Run comprehensive checks across 5 dimensions. Score each dimension 0-4 using the
**Check for**: **Check for**:
- **Fixed widths**: Hard-coded widths that break on mobile - **Fixed widths**: Hard-coded widths that break on mobile
- **Touch targets**: Interactive elements < 44x44px - **Touch targets**: Interactive elements < 44x44px
- **Broken touch interaction**: Custom sliders, drag surfaces, and scrollable control strips whose primary gesture fails under touch, that swallow page scroll or lose the drag to it, or that stay stuck after an interrupted gesture. Code tells: mouse-only handlers, no `touch-action` on a pointer-event drag surface, drag state that nothing clears on cancel, lost capture, or blur. Exercise the gesture when a browser tool can synthesize touch (a rendered viewport proves layout, not the gesture), then say what produced the evidence (emulated viewport, synthesized touch, which engine, physical device) and what stayed untested
- **Horizontal scroll**: Content overflow on narrow viewports - **Horizontal scroll**: Content overflow on narrow viewports
- **Text scaling**: Layouts that break when text size increases - **Text scaling**: Layouts that break when text size increases
- **Missing breakpoints**: No mobile/tablet variants - **Missing breakpoints**: No mobile/tablet variants
**Score 0-4**: 0=Desktop-only (breaks on mobile), 1=Major issues (some breakpoints, many failures), 2=Partial (works on mobile, rough edges), 3=Good (responsive, minor touch target or overflow issues), 4=Excellent (fluid, all viewports, proper touch targets) **Score 0-4**: 0=Desktop-only (breaks on mobile), 1=Major issues (some breakpoints, many failures), 2=Partial (works on mobile, rough edges), 3=Good (responsive, minor touch target or overflow issues), 4=Excellent (fluid, all viewports, proper touch targets, gestures work under touch)
### 5. Implementation Integrity (CRITICAL) ### 5. Implementation Integrity (CRITICAL)
@@ -1,57 +0,0 @@
# Component review
Use this checkpoint on comp-led builds after producing the initial component kit and before composing the page. The approved comp is the reference. The user reviews the actual produced components, including code; a list of planned assets or screenshots supplied by the builder is not a review of what will ship.
## Prepare the component kit
Keep the measured spec's region IDs. Include every visible region: produced raster assets and working HTML/CSS/SVG for text, controls, patterns, decoration and layout elements. A region rendered in code needs an actual review document, not a promise to implement it later. Use semantic HTML for content and controls. Do not flatten the page or combine unrelated regions to avoid review. Report omitted regions so the user can mark what is missing.
Write `.impeccable/review/components.json` with this manifest format:
```json
{
"schemaVersion": 1,
"id": "components",
"title": "Component review",
"stage": "components",
"comp": {"path": ".impeccable/mocks/comp-2.png", "width": 1536, "height": 1024},
"components": [
{
"id": "illustration",
"name": "Illustration",
"medium": "raster",
"box": {"x": 0.5, "y": 0.2, "w": 0.45, "h": 0.7},
"note": "Produced cutout; positioned over the page ground.",
"preview": {"kind": "image", "path": "assets/illustration.png"},
"dependencies": [".impeccable/build/spec.json"]
},
{
"id": "headline",
"name": "Headline",
"medium": "html",
"box": {"x": 0.05, "y": 0.2, "w": 0.4, "h": 0.25},
"note": "Rendered semantic heading and its typography.",
"preview": {"kind": "page", "path": ".impeccable/review/components/headline.html"},
"dependencies": [".impeccable/build/spec.json", "assets/type.woff2"]
}
]
}
```
The coordinates above only illustrate the schema. Use the approved comp's actual pixel dimensions and each measured region's normalized bounds (`x / width`, `y / height`, `w / width`, `h / height`). A code preview is rendered at the comp viewport and cropped to that component's box, so place its content at those coordinates in the review document. Include every file the document uses in `dependencies`, including linked CSS, fonts and images. The runtime also binds the measured spec for the component stage and checks its inventory. Local paths only. Static PNG, WebP and JPEG previews retain their original bytes and actual transparency; never draw a checkerboard into the asset.
Native capture supports stable HTML/CSS and inline SVG. Supply a static review state for motion and keep the implementation's real inputs. A scripted, canvas or otherwise unsupported component is a blocker to report, not permission to substitute a raster or omit it.
## Present and wait
If the harness exposes `component_review`, call it with `manifest_path` set to `.impeccable/review/components.json`. The host captures the component files, presents this same review interface and returns the user's decisions. A suspended request is waiting for the user; it is not a failed build or an approval.
Otherwise run `.cursor/skills/impeccable/scripts/impeccable component-review capture --manifest .impeccable/review/components.json`, then start `.cursor/skills/impeccable/scripts/impeccable component-review serve --session <returned session>` in the background. Open the URL it prints in the available browser and wait for the user. Read the result with `.cursor/skills/impeccable/scripts/impeccable component-review verify --manifest .impeccable/review/components.json`; pending, needs-work and stale input all refuse approval. Never submit the page or write a receipt on the user's behalf.
The user can approve components, request changes, and mark missing regions. Act on their feedback without replacing it with your own favorable verdict. Keep component IDs stable, update the actual implementation and dependency list, and present another round. The UI carries only approvals whose component inputs have not changed. Do not ask the user to reapprove unchanged work. Continue only when the inventory is confirmed and all components are approved.
## Assemble and review
Build the page from the approved component files. Replacing, simplifying or changing an approved component requires a new component review. Run the existing plates and hero gates; human review does not waive their integrity checks.
After the full page and responsive checks are complete, present a second manifest at `.impeccable/review/hero.json`, with `id` and `stage` set to `hero`. Use one page-preview component covering the assembled first viewport, its real HTML entry, and its complete dependency list. The reference stays the approved comp. Call the same host review tool (or native capture/serve/verify workflow) and obtain the user's approval before the final response. Later edits to the reviewed files require a fresh review. A component-kit approval does not approve their assembled layout.
@@ -13,10 +13,6 @@ Do not redesign. Preserve the reference's visual role, silhouette, palette, ligh
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 `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; 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; report a card too thin to brief a comp, never pad it 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 (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 keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. 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. 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 `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; 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; report a card too thin to brief a comp, never pad it 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 (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 keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. 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.
## Review handoff
After the initial production batch, return the actual files and any unresolved drift to the parent for the user-facing component review in [component-review.md](../reference/component-review.md). The parent includes rendered code regions alongside these rasters. A parent or automatic visual check is not a substitute for that human checkpoint. Apply requested repairs and preserve unchanged assets; do not self-approve them. This checkpoint does not apply to the Decision Comps job above.
## Input Contract ## Input Contract
Expect the measured spec (`.impeccable/build/spec.json`, written by `impeccable comp-spec` from the approved comp), the approved comp path, and the skill scripts path. Optionally: a subset of region ids to produce, extra prompt notes per region, and format or transparency needs. Everything else you need is in the spec: each raster region's id, kind (plate, image, texture), pixel box, sampled palette, aspect, note, and the plate path it must land on. Expect the measured spec (`.impeccable/build/spec.json`, written by `impeccable comp-spec` from the approved comp), the approved comp path, and the skill scripts path. Optionally: a subset of region ids to produce, extra prompt notes per region, and format or transparency needs. Everything else you need is in the spec: each raster region's id, kind (plate, image, texture), pixel box, sampled palette, aspect, note, and the plate path it must land on.
@@ -16,7 +16,7 @@ Expect: the original request; the confirmed user answers; the artifact path(s);
## Checks, in order ## Checks, in order
0. **Evidence.** Before any other check, verify the required captures exist and every capture is valid. Required: the platform's full viewport set (web: `desktop.png` and `mobile.png`; native: one capture per shipped device class), plus every capture the calling brief names as required, a reported user viewport (`user-<width>.png`) included. Valid: no black or blank regions, content matching what the filename claims (a visit capture showing the About section is invalid), the document top visible where the file claims a full page, dimensions that make sense for the named viewport. A required capture that is absent fails exactly like one that is malformed: a viewport nobody captured is a viewport nobody inspected, and it cannot ship. When any capture fails, the whole review changes shape: return `disposition: recapture` as the first line, then one section, `recapture`, listing each missing or invalid file and what a valid capture of it shows, and stop. Never build a matrix on malformed evidence; a verdict derived from a broken capture launders the breakage into an approval, and the parent owes you a full re-review on valid captures, not a scoring round. 0. **Evidence.** Before any other check, verify the required captures exist and every capture is valid. Required: the platform's full viewport set (web: `desktop.png` and `mobile.png`; native: one capture per shipped device class), plus every capture the calling brief names as required, a reported user viewport (`user-<width>.png`) included. Valid: no black or blank regions, content matching what the filename claims (a visit capture showing the About section is invalid), the document top visible where the file claims a full page, dimensions that make sense for the named viewport. A required capture that is absent fails exactly like one that is malformed: a viewport nobody captured is a viewport nobody inspected, and it cannot ship. When any capture fails, the whole review changes shape: return `disposition: recapture` as the first line, then one section, `recapture`, listing each missing or invalid file and what a valid capture of it shows, and stop. Never build a matrix on malformed evidence; a verdict derived from a broken capture launders the breakage into an approval, and the parent owes you a full re-review on valid captures, not a scoring round.
1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and every phase before `review` is `closed` or explicitly `skipped`; an open or failed phase is a material finding. A comp-led config with no state file, or a state whose `comps` phase is neither `closed` nor `skipped`, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. 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, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all. 1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and its `comps` (or `skipped` when a surface round locked the comp), `spec`, `plates`, and `hero` phases are `closed`; a comp-led config with no state file, or a state whose `comps` phase never closed, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. 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, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all.
2. **Fidelity.** Start from the measurement, then judge what it cannot: read `.impeccable/review/diff/final/report.json` (and hero) first; every region scored `missing` or `contradicted` is a matrix row in that state unless the paired crop under `regions/` shows the score is wrong, and you say why; a region scored `match` still gets your eye for lettering character and material, which the numbers do not measure. Then, 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; 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. Three 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; medium is part of the promise. GROUND: the page field's value and temperature against the comp's, sampled from pixels on both sides when tooling allows rather than judged from memory, and read as the net on-screen result where a texture or tile paints over the base color; a ground warmer or cooler than the comp's is contradicted however faithfully the layout matches, and drift toward the rendition prior (warm cream on light grounds, blue-black slate on dark) is the direction to hunt. With no approved comp, 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 renders) as contradicted on its face; imitation material is the single most reliable mark of machine-made design. GROUND narrows rather than lapses: with no comp to sample, a color OWN-WORLD names is the target and the same warmer-or-cooler judgment applies; when OWN-WORLD names none, there is no GROUND authority, and the review says so in place of a verdict, because a target the reviewer invents turns the check into taste. A critique-reference comp on such a build is provocation, not spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is what the image dared that the build did not, and 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. A fix that requires producing an asset says so explicitly ("produce: <region> 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. 2. **Fidelity.** Start from the measurement, then judge what it cannot: read `.impeccable/review/diff/final/report.json` (and hero) first; every region scored `missing` or `contradicted` is a matrix row in that state unless the paired crop under `regions/` shows the score is wrong, and you say why; a region scored `match` still gets your eye for lettering character and material, which the numbers do not measure. Then, 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; 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. Three 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; medium is part of the promise. GROUND: the page field's value and temperature against the comp's, sampled from pixels on both sides when tooling allows rather than judged from memory, and read as the net on-screen result where a texture or tile paints over the base color; a ground warmer or cooler than the comp's is contradicted however faithfully the layout matches, and drift toward the rendition prior (warm cream on light grounds, blue-black slate on dark) is the direction to hunt. With no approved comp, 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 renders) as contradicted on its face; imitation material is the single most reliable mark of machine-made design. GROUND narrows rather than lapses: with no comp to sample, a color OWN-WORLD names is the target and the same warmer-or-cooler judgment applies; when OWN-WORLD names none, there is no GROUND authority, and the review says so in place of a verdict, because a target the reviewer invents turns the check into taste. A critique-reference comp on such a build is provocation, not spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is what the image dared that the build did not, and 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. A fix that requires producing an asset says so explicitly ("produce: <region> 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. 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, 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. 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, 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.
@@ -0,0 +1,101 @@
> **Additional context needed**: only the target element, when the request does not name one that resolves uniquely on the page.
Generate is the fast lane into live mode: the user names an element, a direction, and a count in one sentence, and within a minute they are cycling through variants in their browser. One command boots the helper, hands the element to the overlay in the page your harness already shows (it scrolls to it, selects it, and fires the same Go a click fires) and returns the generate event; one edit writes the variants; one call replies and waits for the user's choice, which the helper bakes into source itself. This file owns the lane's plumbing; from the event onward the design work is [live.md](live.md)'s, unchanged, so read it in full now if you have not this session.
**Web only.** Live mode's browser overlay has no native equivalent; on `ios` / `android` / `adaptive` projects, decline this command and offer `bolder` or `quieter` on the source instead.
The plumbing is where the lane saves time: one command starts the session around the page your harness already shows, one call replies and waits, and nothing here is a browser you have to babysit. The design work is not where it saves time. Setup runs as for any command (`impeccable context`, this reference, craft-floor.md before the edit), and the variants are planned, written, and accepted exactly the way a live session plans, writes, and accepts them.
Three prohibitions cover the known ways this command goes wrong:
- **Never run init or document, and never ask for PRODUCT.md or DESIGN.md.** When they exist, the start command prints them under `boot` and you use them. When they do not, it says so (`contextMissing`, `contextNote`) and you extract the identity from the event (Step 3). A missing file is never a reason to interview the user inside this command; offer `init` in one line after the session ends.
- **Never hand-write a variants wrapper or invent a session id.** Only the browser mints session ids (8 hex characters, at Go). A missing event is fixed by rerunning Step 2, never with a direct source edit.
- **Do not act on hook findings while live markers are in the file**, and do not restyle variants to appease them; the accept verifies the file once the variant is permanent.
## Step 1: Parse the request
Three parts, all from the user's sentence:
- **A number in the request**: that is the count. **No number**: 3. The protocol caps count at 8.
- **The direction wording** maps onto the live action vocabulary; never invent a new action value:
- **bold, bolder, stronger, punchier**: `bolder`
- **quiet, calmer, softer, toned down**: `quieter`
- **simpler, minimal, stripped**: `distill`
- **refined, tightened, polished**: `polish`
- **font and type words**: `typeset`
- **color words**: `colorize`
- **arrangement and spacing words**: `layout`
- **device and breakpoint words**: `adapt`
- **motion words**: `animate`
- **playful words**: `delight`
- **rule-breaking words**: `overdrive`
- **Wording that carries intent but no vocabulary word** ("make it feel like a bank", "warmer", "more premium"): `impeccable`, with the user's wording passed as the prompt.
- **An action fits AND extra intent rides along** ("bolder, but keep it monochrome"): that action, with the rest as the prompt.
- **The wording names no direction at all** ("better", "improve", "nicer", "different", "fresh", "new", "redesign", "fix", "some options", "ideas", "alternatives", or just "variants" with nothing else): Ask the user directly to clarify what you cannot infer. Ask one question, offering the vocabulary: *"Which direction should the variants take? bolder, quieter, simpler (distill), polished, typography (typeset), color (colorize), layout, motion (animate), playful (delight), or rule-breaking (overdrive)."* Map the answer with this list; an answer that is still open ("surprise me", "you pick") is `impeccable` with the user's original wording as the prompt, and Step 2 starts on that answer.
- **The element description** ("the pricing cards", "the hero heading"): Step 2 resolves it to a selector.
Done when you hold an action from the vocabulary (asked for, when the request named no direction), a count from 1 to 8, and the element description.
## Step 2: Reuse the page, then start
**Reuse** the dev server already running and the tab your harness already shows it in; a second server or a second browser window is the failure this step prevents.
1. **Find the dev server**, cheapest source first, and stop at the first hit: the user's message, a browser tab already on the app (Claude Code: an origin in `tabs_context`), a server your harness started (Claude Code: `preview_list`), a terminal that printed its URL. Its origin is your `--dev-url`. **No hit**: leave `--dev-url` off and run the start command with no wait; the boot probes for a running server and its verdict names the move. `browser_needed` carries the `devUrl` it found: open it as in 2, then rerun with `--dev-url <devUrl> --wait-for-browser 60000`. `no_dev_server` means nothing serves the app: start the dev script the way the verdict says (Claude Code: `preview_start`; Cursor: a background terminal; Codex: an exec you yield from), wait for its URL, then rerun with `--dev-url <url>`.
2. **Open the page that renders the element in your browser, then start.** The route the request names, else the one `--target` serves; `--dev-url` takes only the origin.
- **Cursor** (`browser_navigate`) and **Claude Code** (`navigate`, which opens the Browser pane when it is closed and takes the `tabId` from `tabs_context` when a tab is already on that origin): open the URL, then run the start command with `--dev-url <url> --wait-for-browser 60000`. The boot injects the overlay and the page reloads into it while the command waits. Your browser tool is the only opener on these harnesses; the engine ignores `--open` there.
- **No browser tool** (Codex, others): run the start command with `--open --wait-for-browser 120000`; it opens the system browser, and the longer wait covers the user finding the tab. **`browser_open_failed` back**: tell the user the `url` in one line and rerun with `--wait-for-browser 120000`.
```bash
.cursor/skills/impeccable/scripts/impeccable live-generate --target src/App.jsx --dev-url http://127.0.0.1:5173/ --selector ".pricing-grid" --action bolder --count 3 --boot --wait-for-browser 60000
```
Run it in the foreground in Cursor and Claude Code (it returns within the wait); on Codex, in an exec you yield from, the way Step 3 runs the poll.
- `--target`: the file that renders the element when the request or the project makes it obvious; skip it otherwise.
- `--dev-url`: the origin from 1; omit it and the boot probes.
- `--selector`: a unique class first, then a landmark tag plus class, an id last (every variant mounts a copy of the element, so an id repeats in the DOM). **The request names a repeated component in plural** ("the pricing cards"): target the container that holds the set, so one scoped stylesheet restyles every instance. One read of the source file that renders the element is allowed when the selector is not obvious; `--dry-run` resolves and reports without starting anything when it is not certain.
- `--boot`: runs the lane's boot (PRODUCT.md and DESIGN.md loaded again for the helper, missing files tolerated, dev URL found, bottom bar hidden for the helper's lifetime) and reuses a helper that is already running. Its result rides along as `boot`.
- Also available: `--prompt`, `--text` (keep only matches whose visible text contains a snippet), `--index` (1-based pick among matches).
Read the output in this order: `boot` (or `boot.contextMissing` with `boot.contextNote`: the page is the source of truth, per the note), then `event`, the generate event for `sessionId`, with the same `_instructions` a user's Go gets. Every verdict carries `_instructions`, and they win over your recollection of this file; the ones whose move is a decision of yours:
- **`ambiguous`**: the candidates are listed; target their common container, or rerun with `--text "<visible text>"` or `--index <n>`.
- **`dev_server_gone`**: the dev server stopped answering while the command waited for the page (on Cursor, a server another chat started dies with that chat). Start it the way the verdict says, then rerun with `--dev-url <url>`.
- **`no_match`**: the tab is on a route that does not render the element (navigate to the right route, rerun), or the selector is wrong (derive a better one from the source, or add `--text`).
- **`config_missing` / `config_invalid`** under `bootError`: follow [live-setup.md](live-setup.md) first, then rerun.
- **`event: null`** with `ok: true`: the event was slower than the wait; run `.cursor/skills/impeccable/scripts/impeccable live-poll` once to collect it, then continue.
Done when the output shows `ok: true`, a `sessionId`, and an `event`, reached with at most one server started and one tab opened by you.
## Step 3: Generate
The event is a standard `generate` event: the picked element's context, a preflighted scaffold, and `_instructions` naming the action's reference, the planning section, and the exact splice. Handle it exactly per live.md's **Handle generate**, which owns everything from the identity lock to the done reply: read the action's reference and craft-floor.md as it says, plan per section 4 (identity first, then mode, then three different primary axes, then the squint test), declare knobs per section 7, and deliver per section 6 (a complete replacement of the element per variant, the preview CSS plus every variant in one edit at the scaffold's splice). The lane changes nothing about what a variant may be: the moves a live session would make on this element (a promoted tier, a restructured set, a reordered card, a different surface) are open here too. Never screenshot the page; the overlay preview is the review channel until accept.
**Reply and wait in one call**, with the file you wrote:
```bash
.cursor/skills/impeccable/scripts/impeccable live-poll --reply EVENT_ID done --file src/App.jsx --then-poll
```
This replies done (the browser mounts the variants) and then blocks until the user's choice arrives, so run it the way your harness runs a long wait: **Claude Code** in the foreground with your tool's longest timeout (600000 ms), so you are paused until the choice arrives; **Codex** in a yielded foreground exec; **Cursor** in a background terminal with notify on `"type":"(accept|discard|variant_mount_failed|exit)"`. Never pass a short `--timeout=`. While it runs there is nothing else to do: never sleep and never poll its output on a timer; a harness that backgrounds it wakes you when it returns. `{"type":"timeout"}` means the user has not chosen yet: run `live-poll` again and keep waiting. If the edit fails after the browser flipped to GENERATING, `--reply EVENT_ID error "Short reason"` (without `--then-poll`) so the bar resets.
Then tell the user, in one line, where their variants are: *"Three [bolder] variants are live on [the pricing cards]: cycle with the floating bar's arrows, adjust the Tune knobs, and Accept the keeper."*
Outside the replace path, read the matching live.md section before acting: `scaffold.previewMode: "svelte-component"` (Svelte previews are edited as components, and their accept is mechanical), `mode: "insert"`, `variant_mount_failed`, `steer`, `manual_edit_apply`, and any `fallback: "agent-driven"` wrap error.
## Step 4: Accept and close
The call from Step 3 returns the user's choice. **`discard`**: nothing to do. **`accept`**: `_acceptResult.carbonize: true` is the normal case, and the cleanup is live.md's **Required after accept**, unchanged: move the accepted variant's rules into the stylesheet that already owns the element with real selectors, bake the chosen knob values in, unwrap the element and drop every `data-impeccable-*` attribute, delete the inline `<style>` block and both `impeccable-carbonize` markers, then `.cursor/skills/impeccable/scripts/impeccable live-complete --id SESSION_ID` and confirm `phase: "completed"`. (`baked: true` appears only when the accept was run with `--bake`; then the helper already made the variant permanent and no `live-complete` is owed.)
Close without being asked, the moment the choice is handled:
```bash
.cursor/skills/impeccable/scripts/impeccable live-server stop
```
Stopping removes the injected script and reloads the page once: the user sees the accepted design with no overlay chrome, still served by their dev server. **Never kill or restart the dev server**, including one you started in Step 2.
- **The user asks for more variants before you closed**: skip the close, run Step 2 again for the next element (the helper is reused), and close after the last choice.
- **Interrupted or unsure of the state**: `.cursor/skills/impeccable/scripts/impeccable live-status`, then `live-resume`; the journal under `.impeccable/live/sessions/` is canonical.
Done when the helper is stopped and the dev site still answers with the accepted design.
@@ -205,6 +205,11 @@ t('items', { count }) // Handles complex plural rules
- Optimistic updates with rollback - Optimistic updates with rollback
- Conflict resolution - Conflict resolution
**Interrupted gestures** (custom sliders, drag surfaces, scrollable control strips):
- A second finger or pointer lands mid-drag: the first drag keeps its pointer or ends cleanly, never jumps to the new one
- The browser cancels the gesture to scroll (`pointercancel`), capture is lost (`lostpointercapture`), the pointer is released outside the control, or the window loses focus (`blur`) mid-drag: clear the dragging state and release capture
- After each of these, the next tap or drag works without a reload
**Permission states**: **Permission states**:
- No permission to view - No permission to view
- No permission to edit - No permission to edit
@@ -304,6 +309,7 @@ const throttledScroll = throttle(handleScroll, 100);
- Unit tests for edge cases - Unit tests for edge cases
- Integration tests for error scenarios - Integration tests for error scenarios
- E2E tests for critical paths - E2E tests for critical paths
- A behavioral regression for each confirmed gesture fix, when the project's test runner can drive input
- Visual regression tests - Visual regression tests
- Accessibility tests (axe, WAVE) - Accessibility tests (axe, WAVE)
@@ -330,7 +336,10 @@ Test thoroughly with edge cases:
- **Network issues**: Disable internet, throttle connection - **Network issues**: Disable internet, throttle connection
- **Large datasets**: Test with 1000+ items - **Large datasets**: Test with 1000+ items
- **Concurrent actions**: Click submit 10 times rapidly - **Concurrent actions**: Click submit 10 times rapidly
- **Interrupted gestures**: Add a second finger mid-drag, scroll across the control, release outside it, switch windows mid-drag; then drag again
- **Errors**: Force API errors, test all error states - **Errors**: Force API errors, test all error states
- **Empty**: Remove all data, test empty states - **Empty**: Remove all data, test empty states
For gestures, say what produced the evidence (emulated viewport, synthesized touch, which engine, physical device) and name what stayed untested.
When edge cases are covered, hand off to `/impeccable polish` for the final pass. When edge cases are covered, hand off to `/impeccable polish` for the final pass.
@@ -96,7 +96,7 @@ Build the assigned direction, not a safer interpretation of it. The form supplie
When an approved comp exists, it is a spatial contract, not a mood board: only the user can downgrade its authority, in explicit words. Models systematically believe their HTML, CSS, and SVG recreation of an image succeeded when it did not, so the build runs as a state machine on disk whose gates measure the screen against the comp instead of asking you to remember it. Start it once, and let it tell you what is next: When an approved comp exists, it is a spatial contract, not a mood board: only the user can downgrade its authority, in explicit words. Models systematically believe their HTML, CSS, and SVG recreation of an image succeeded when it did not, so the build runs as a state machine on disk whose gates measure the screen against the comp instead of asking you to remember it. Start it once, and let it tell you what is next:
`.cursor/skills/impeccable/scripts/impeccable build-phase start --direction <seed key> --kind <assigned|pick|challenger|canon> --artifact <entry file>` right after the direction choice (this is also the choice ping; the roll's output names the exact command), or `start --comp <approved comp> --artifact <entry file>` when a surface round already locked one. `.cursor/skills/impeccable/scripts/impeccable build-phase start --direction <seed key> --kind <assigned|pick|challenger|canon>` right after the direction choice (this is also the choice ping; the roll's output names the exact command), or `start --comp <approved comp>` when a surface round already locked one.
Then, in order, each closed by `.cursor/skills/impeccable/scripts/impeccable build-phase advance` (every verb below runs as `.cursor/skills/impeccable/scripts/impeccable <verb>`; exit 2 means the gate failed and printed why; fix that and advance again; write nothing for a later phase while an earlier gate is open): Then, in order, each closed by `.cursor/skills/impeccable/scripts/impeccable build-phase advance` (every verb below runs as `.cursor/skills/impeccable/scripts/impeccable <verb>`; exit 2 means the gate failed and printed why; fix that and advance again; write nothing for a later phase while an earlier gate is open):
@@ -104,9 +104,8 @@ Then, in order, each closed by `.cursor/skills/impeccable/scripts/impeccable bui
The comp-led path is a frontier-tier job: it asks the builder to hold a measured layout, place plates at their boxes, and act on numeric readings across a dozen attempts. Smaller or faster models produce a recognisable page and stall under the hero gate; if the model in hand is one of those, say so before the direction round and take the code-led path, or expect the run to end at the hero with its readings unmet. The comp-led path is a frontier-tier job: it asks the builder to hold a measured layout, place plates at their boxes, and act on numeric readings across a dozen attempts. Smaller or faster models produce a recognisable page and stall under the hero gate; if the model in hand is one of those, say so before the direction round and take the code-led path, or expect the run to end at the hero with its readings unmet.
1. **spec.** Measure the comp: `impeccable comp-spec --comp <comp> --grid` writes a coordinate grid over the comp; open it, name every salient region by grid span in a regions file (text and control regions snap to the largest ink mass inside their span, so a headline named B1:E4 measures as the headline and not the column beside it; `snap: false` keeps the span, and an explicit `box` is taken as drawn) (kind `plate` / `image` / `texture` for anything painted: every illustration, photograph, figure, product object, and material texture; `text` / `control` / `chrome` for what code draws; every region carries a `note` saying what the comp shows there, which the plate prompt and the gate messages read), and run `impeccable comp-spec --comp <comp> --regions <file>`. The spec carries each region's box, sampled palette, and medium; `impeccable comp-spec --print` is the build's reference from here on. Type is measured, not guessed: `impeccable font-match --measure <text region>` reads the comp's cap height, width class, and weight off the pixels, and `impeccable font-match --rank <region> --text "..."` takes its candidates from a fingerprint index of the Google Fonts catalog (the nearest faces to the crop's shape) plus any names you pass with `--candidates`, renders them at that cap height with the region's words, and ranks them by fingerprint distance (its `USE` line is the CSS; its proof sheet shows the comp over the top three); with no browser resolvable it records the catalog's nearest face and says the size is estimated, which is still the choice to build on. Do not install a browser to rank, and never write a `chosen` face into the spec by hand: the gate accepts only what font-match wrote. The spec gate refuses to close until the lead text region is measured and ranked. A region note that describes painted material (a diagram, drawing, photograph, texture) under a code kind is refused at the spec: reclassify it as a plate, or reword the note if code really draws it. The script refuses a regions file that leaves comp ink unnamed (callouts, a parts table, a notes block): what is never named can never be missing, so everything the comp shows gets a region. It also refuses a `text` / `control` / `chrome` region larger than a quarter of the comp: that is a column, not an element, and a column scored as one region hides the plates, tables, and notes inside it. Name each element inside it (`container: true` only when it truly is one undivided element). Anything drawn is a plate: an inline SVG past an icon's budget (a diagram, notation, leader lines with arrows, a "quick approximation" of the artwork) is refused at the hero; icon-sized SVG (under 64px, a few paths) is fine, and a chart the page draws from data at runtime is a chart, not an illustration. Callout lines and arrows that annotate a drawing belong to that drawing's plate, with only their labels set as text. A crop of the comp is never a plate (the plates gate refuses a file that is a resample of the comp region: the comp's grain, its neighbours' edges, and its resolution would ship as the artwork); the crop is the reference the plate is generated from. A plate region's box has to hold its whole artwork with a margin: the spec measures the artwork's contact with the box edges and refuses a box that cuts through it (`bleed: true` only when the page really crops it there), because a plate placed with `object-fit: cover` on such a box shows the artwork minus the side the box lost. Anything not in the spec does not exist on the page: no borders, rules, containers, or chrome the comp does not show. Only three concessions exist: fonts (the closest obtainable face), icon glyphs (close enough, exact if the user chose an icon library; this covers the pictogram only, never a control's chrome, so a chevron, an arrow, a dropdown's border and fill, a button's shape are the comp's), and genuine defects in the comp such as spelling errors. 1. **spec.** Measure the comp: `impeccable comp-spec --comp <comp> --grid` writes a coordinate grid over the comp; open it, name every salient region by grid span in a regions file (text and control regions snap to the largest ink mass inside their span, so a headline named B1:E4 measures as the headline and not the column beside it; `snap: false` keeps the span, and an explicit `box` is taken as drawn) (kind `plate` / `image` / `texture` for anything painted: every illustration, photograph, figure, product object, and material texture; `text` / `control` / `chrome` for what code draws; every region carries a `note` saying what the comp shows there, which the plate prompt and the gate messages read), and run `impeccable comp-spec --comp <comp> --regions <file>`. The spec carries each region's box, sampled palette, and medium; `impeccable comp-spec --print` is the build's reference from here on. Type is measured, not guessed: `impeccable font-match --measure <text region>` reads the comp's cap height, width class, and weight off the pixels, and `impeccable font-match --rank <region> --text "..."` takes its candidates from a fingerprint index of the Google Fonts catalog (the nearest faces to the crop's shape) plus any names you pass with `--candidates`, renders them at that cap height with the region's words, and ranks them by fingerprint distance (its `USE` line is the CSS; its proof sheet shows the comp over the top three); with no browser resolvable it records the catalog's nearest face and says the size is estimated, which is still the choice to build on. Do not install a browser to rank, and never write a `chosen` face into the spec by hand: the gate accepts only what font-match wrote. The spec gate refuses to close until the lead text region is measured and ranked. A region note that describes painted material (a diagram, drawing, photograph, texture) under a code kind is refused at the spec: reclassify it as a plate, or reword the note if code really draws it. The script refuses a regions file that leaves comp ink unnamed (callouts, a parts table, a notes block): what is never named can never be missing, so everything the comp shows gets a region. It also refuses a `text` / `control` / `chrome` region larger than a quarter of the comp: that is a column, not an element, and a column scored as one region hides the plates, tables, and notes inside it. Name each element inside it (`container: true` only when it truly is one undivided element). Anything drawn is a plate: an inline SVG past an icon's budget (a diagram, notation, leader lines with arrows, a "quick approximation" of the artwork) is refused at the hero; icon-sized SVG (under 64px, a few paths) is fine, and a chart the page draws from data at runtime is a chart, not an illustration. Callout lines and arrows that annotate a drawing belong to that drawing's plate, with only their labels set as text. A crop of the comp is never a plate (the plates gate refuses a file that is a resample of the comp region: the comp's grain, its neighbours' edges, and its resolution would ship as the artwork); the crop is the reference the plate is generated from. A plate region's box has to hold its whole artwork with a margin: the spec measures the artwork's contact with the box edges and refuses a box that cuts through it (`bleed: true` only when the page really crops it there), because a plate placed with `object-fit: cover` on such a box shows the artwork minus the side the box lost. Anything not in the spec does not exist on the page: no borders, rules, containers, or chrome the comp does not show. Only three concessions exist: fonts (the closest obtainable face), icon glyphs (close enough, exact if the user chose an icon library; this covers the pictogram only, never a control's chrome, so a chevron, an arrow, a dropdown's border and fill, a button's shape are the comp's), and genuine defects in the comp such as spelling errors.
2. **plates.** Every raster region ships as a plate: an illustration, photo, or figure regenerated at asset resolution from its comp crop, UI text removed, at its `plate` path (isolated ink, figures, or objects use native transparent PNG so they sit on the page's own ground; photos and textures stay opaque); a texture (paper, cloth, grain) is a clean patch of the comp region mirror-tiled to size, generated only when no clean patch exists. `impeccable comp-spec --crop <id>` writes the reference; save `impeccable comp-spec --plate-prompt <id> --background transparent` to a prompt file for a cutout, or use `--background opaque` otherwise. Prefer the harness-native image tool with that crop and prompt, then `impeccable embed-prompt <plate> --prompt-file <prompt.txt>`. The API fallback is `impeccable generate-image --ref <crop.png> --prompt-file <prompt.txt> --out <plate.png> --size <WxH> --quality high --background transparent` (use `--background opaque` for full-frame assets); create the output directory first. Verify actual alpha, white foregrounds, fine edges, and clear holes on light and dark grounds; do not chroma-key native output. After the initial assets exist, prepare the isolated code component previews and complete [component-review.md](component-review.md) before further gate-driven repair: the user reviews the whole component kit, including code, before page assembly. This checkpoint keeps the existing gates; it does not require passing them first. After the full page and responsive checks, use the assembled-hero checkpoint before the final response. The plates gate scores the assets against the comp; also inspect placement and scale visually. With parallel subagents, spawn the shipped asset producer (`impeccable-asset-producer`; `impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent") with the spec path and let it produce them all; without subagents, produce them here. A crop of the comp is a reference, never a shipping pixel. The gate checks every plate exists, is at least 1.5x the region's size, and reads as the region. Page code waits for this gate: a page written before its plates exist is a page that draws its material in CSS. A single-file deliverable changes nothing here: the plate is produced the same way and inlined as a data URI. `--force` exists for one case only, the user downgrading the comp's authority in words you quote in `--reason`; the script refuses every other reason. 2. **plates.** Every raster region ships as a plate: an illustration, photo, or figure regenerated at asset resolution from its comp crop, UI text removed, at its `plate` path (isolated ink, figures, or objects use native transparent PNG so they sit on the page's own ground; photos and textures stay opaque); a texture (paper, cloth, grain) is a clean patch of the comp region mirror-tiled to size, generated only when no clean patch exists. `impeccable comp-spec --crop <id>` writes the reference; save `impeccable comp-spec --plate-prompt <id> --background transparent` to a prompt file for a cutout, or use `--background opaque` otherwise. Prefer the harness-native image tool with that crop and prompt, then `impeccable embed-prompt <plate> --prompt-file <prompt.txt>`. The API fallback is `impeccable generate-image --ref <crop.png> --prompt-file <prompt.txt> --out <plate.png> --size <WxH> --quality high --background transparent` (use `--background opaque` for full-frame assets); create the output directory first. Verify actual alpha, white foregrounds, fine edges, and clear holes on light and dark grounds; do not chroma-key native output. The plates gate scores the assets against the comp; also inspect placement and scale visually. With parallel subagents, spawn the shipped asset producer (`impeccable-asset-producer`; `impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent") with the spec path and let it produce them all; without subagents, produce them here. A crop of the comp is a reference, never a shipping pixel. The gate checks every plate exists, is at least 1.5x the region's size, and reads as the region. Page code waits for this gate: a page written before its plates exist is a page that draws its material in CSS. A single-file deliverable changes nothing here: the plate is produced the same way and inlined as a data URI. `--force` exists for one case only, the user downgrading the comp's authority in words you quote in `--reason`; the script refuses every other reason.
3. **hero.** `impeccable build-phase scaffold` first: it writes the measured layout as CSS custom properties (`.impeccable/build/scaffold/layout.css`: `--r-<id>-x/y/w/h` in % of the comp, plus cap height, font-size, family, and weight where measured) and a reference page (`hero-reference.html`) with every region at its box and every plate placed. Bind the numbers to your own semantic structure, an element per region; the reference is a check on positions, never the page, and overlapping boxes are overlapping boxes. Then build only the first viewport, at the comp's own dimensions, the comp's words copied verbatim (the user approved that comp with those words; rewording is a stated decision after the hero passes, never a silent one inside it), every text region sized from its measured cap height and set in its ranked face, plates first: place every plate at its spec box (`object-fit: cover`, an `<img>`, a background image, or an inlined data URI named for it) before any text or control, capture into `.impeccable/review/hero-repro.png`, run `impeccable build-phase record hero` once so you see the plate regions read as match before any text exists, then lay the semantic layer over the plates from the spec's palette and boxes and advance. The gate first refuses while any plate is unreferenced by the source, then runs `impeccable comp-diff`, writes `.impeccable/review/diff/hero/` (side-by-side, heatmap, one paired crop per region, `report.json`), and passes at 72% overall with no hard veto outstanding (a missing region, a contradicted plate or text block, an SVG illustration, a clipped plate, invented ink block at any score); above the bar, the numeric readings become advisories printed with the pass, and the polish pass before responsive is where they get fixed: the gate also reads each text region's cap height, line count, weight, ink colour, and position against the comp, each chrome strip's height off its rule, and the frame for ink where the comp is calm (a kicker, an extra nav item, a divider), and says each miss as a number ("cap height 78px in the build, 103px in the comp"); those numbers are the edit. When it fails, open the region crops it lists, in order, before editing: a region scored `missing` needs its material, `contradicted` needs its structure re-derived from the spec box, `drift` is where size and spacing edits belong; the gate refuses a third attempt that only nudges values on the same region. This is where the run's ambition is won or lost, and a retry here costs minutes where a rebuild verdict at the finish costs the run.
3. **hero.** `impeccable build-phase scaffold` first: it writes the measured layout as CSS custom properties (`.impeccable/build/scaffold/layout.css`: `--r-<id>-x/y/w/h` in % of the comp, plus cap height, font-size, family, and weight where measured) and a reference page (`hero-reference.html`) with every region at its box and every plate placed. Bind the numbers to your own semantic structure, an element per region; the reference is a check on positions, never the page, and overlapping boxes are overlapping boxes. Then build only the first viewport, at the comp's own dimensions, the comp's words copied verbatim (the user approved that comp with those words; rewording is a stated decision after the hero passes, never a silent one inside it), every text region sized from its measured cap height and set in its ranked face, plates first: place every plate at its spec box (`object-fit: cover`, an `<img>`, a background image, or an inlined data URI named for it) before any text or control, capture into `.impeccable/review/hero-repro.png`, run `impeccable build-phase record hero` once so you see the plate regions read as match before any text exists, then lay the semantic layer over the plates from the spec's palette and boxes and advance. The gate first refuses while any plate is unreferenced by the source, then runs `impeccable comp-diff`, writes `.impeccable/review/diff/hero/` (side-by-side, heatmap, one paired crop per region, `report.json`; `raw-report.json` preserves the uninterpreted measurements). The report and crop labels use the gate's verdicts; `gate.reasons` lists the remaining blockers even when a region is called drift. An accepted plate is revalidated if its file, measured region, or comp changes. The gate passes at 72% overall with no hard veto outstanding (a missing region, a contradicted plate or text block, an SVG illustration, a clipped plate, invented ink block at any score); above the bar, the numeric readings become advisories printed with the pass, and the polish pass before responsive is where they get fixed: the gate also reads each text region's cap height, line count, weight, ink colour, and position against the comp, each chrome strip's height off its rule, and the frame for ink where the comp is calm (a kicker, an extra nav item, a divider), and says each miss as a number ("cap height 78px in the build, 103px in the comp"); those numbers are the edit. When it fails, open the region crops it lists, in order, before editing: a region scored `missing` needs its material, `contradicted` needs its structure re-derived from the spec box, `drift` is where size and spacing edits belong; repeated attempts do not clear unresolved blockers. This is where the run's ambition is won or lost, and a retry here costs minutes where a rebuild verdict at the finish costs the run.
4. **sections.** Build the rest of the surface inside the spec's system: the same corner language, line weights, and palette, and nothing the comp never shows. Where the comp does not cover a region, it inherits the recorded system. 4. **sections.** Build the rest of the surface inside the spec's system: the same corner language, line weights, and palette, and nothing the comp never shows. Where the comp does not cover a region, it inherits the recorded system.
5. **motion.** The signature interaction, reveals, and motion, orchestrated once rather than scattered. 5. **motion.** The signature interaction, reveals, and motion, orchestrated once rather than scattered.
6. **responsive.** The other viewports, and the first viewport at common desktop widths (1280 to 1600), not only at the comp's exact size: fluid columns, no fixed-pixel grid that wraps a hundred pixels narrower. Capture `desktop.png` (1440 wide, full page) and `mobile.png` (390 wide) into `.impeccable/review/`; the gate diffs the desktop capture against the comp and refuses a first viewport that only held at the comp's width. A comp'd surface that is mobile-first was comped portrait; the plates were produced for that frame. 6. **responsive.** The other viewports, and the first viewport at common desktop widths (1280 to 1600), not only at the comp's exact size: fluid columns, no fixed-pixel grid that wraps a hundred pixels narrower. Capture `desktop.png` (1440 wide, full page) and `mobile.png` (390 wide) into `.impeccable/review/`; the gate diffs the desktop capture against the comp and refuses a first viewport that only held at the comp's width. A comp'd surface that is mobile-first was comped portrait; the plates were produced for that frame.
@@ -144,5 +143,3 @@ A rebuild and a fix round share one asset rule: a raster either round creates or
Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice.
After the last correction, spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, artifact path, direction contract, PRODUCT.md, [document.md](document.md), and write boundary. Without subagents, load [degraded/documenter.md](degraded/documenter.md) and [document.md](document.md) before writing. Verify the outcome: new worlds and approved system changes require token-bearing DESIGN.md **and** `.impeccable/design.json`, not prose alone. Ordinary extensions compare the finished build against the incumbent system, preserve its files, and report the evidence checked; report pre-existing drift without repairing it unasked. Recheck after later edits. Finish only when review and documentation are complete. After the last correction, spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, artifact path, direction contract, PRODUCT.md, [document.md](document.md), and write boundary. Without subagents, load [degraded/documenter.md](degraded/documenter.md) and [document.md](document.md) before writing. Verify the outcome: new worlds and approved system changes require token-bearing DESIGN.md **and** `.impeccable/design.json`, not prose alone. Ordinary extensions compare the finished build against the incumbent system, preserve its files, and report the evidence checked; report pre-existing drift without repairing it unasked. Recheck after later edits. Finish only when review and documentation are complete.
On a comp-led build, record the final review disposition with `.cursor/skills/impeccable/scripts/impeccable build-phase finish --disposition <ship|fix|rebuild|recapture>` before the final response. A refused `ship` is an unfinished build; report the outstanding phase with the verdict.
@@ -16,7 +16,7 @@ Reason over the signals; there is no score to obey:
- `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique <surface>` is a strong default. - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique <surface>` is a strong default.
- `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog and closes it when stale or cleared). - `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog and closes it when stale or cleared).
- `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them.
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. **`live` and the bundled `impeccable detect` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with either; the browser overlay and the HTML rule engine don't apply to native app code. - `devServer.running` true → `live` is available for in-browser iteration, and `generate` for one-shot variant runs on a named element; if false, don't lead with either. **`live`, `generate`, and the bundled `impeccable detect` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with any of them; the browser overlay and the HTML rule engine don't apply to native app code.
- Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`. - Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`.
**If `scan.targets` is non-empty and `setup.platform` is not `ios`/`android`/`adaptive`, run `.cursor/skills/impeccable/scripts/impeccable detect --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx; it reads HTML/CSS, so skip it for native projects). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it. **If `scan.targets` is non-empty and `setup.platform` is not `ios`/`android`/`adaptive`, run `.cursor/skills/impeccable/scripts/impeccable detect --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx; it reads HTML/CSS, so skip it for native projects). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
+1 -1
View File
@@ -1 +1 @@
0.1.6 0.1.5
@@ -19,6 +19,10 @@
"description": "Interactive live variant mode. Select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via HMR. Requires a running dev server. Use when you want to visually experiment with design alternatives in real time.", "description": "Interactive live variant mode. Select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via HMR. Requires a running dev server. Use when you want to visually experiment with design alternatives in real time.",
"argumentHint": "" "argumentHint": ""
}, },
"generate": {
"description": "Agent-driven live variant generation. Boots live mode, finds the named element on the open page, scrolls the browser to it, and delivers N variants in the requested direction for the user to cycle and accept. Use for requests that name an element and a direction, like 'generate 3 bold variants of the pricing cards', skipping manual element picking.",
"argumentHint": "[count] [direction] variants of [element]"
},
"adapt": { "adapt": {
"description": "Adapt designs to work across different screen sizes, devices, contexts, or platforms. Implements breakpoints, fluid layouts, and touch targets. Use when the user mentions responsive design, mobile layouts, breakpoints, viewport adaptation, or cross-device compatibility.", "description": "Adapt designs to work across different screen sizes, devices, contexts, or platforms. Implements breakpoints, fluid layouts, and touch targets. Use when the user mentions responsive design, mobile layouts, breakpoints, viewport adaptation, or cross-device compatibility.",
"argumentHint": "[target] [context (mobile, tablet, print...)]" "argumentHint": "[target] [context (mobile, tablet, print...)]"
@@ -165,6 +165,14 @@
} }
let parameterGenerationState = 'idle'; let parameterGenerationState = 'idle';
let parameterReadyAnnouncedSession = null; let parameterReadyAnnouncedSession = null;
// 'agent' when the generate verb fired this session's Go (the generate
// lane declares no knobs, so its bar never shows a pending Tune chip);
// null for every Go a user presses.
let sessionOrigin = null;
// The generate lane picks for the agent and never edits copy in the
// browser, so its selection carries no edit-copy badge (set on the
// agent-target pick, cleared with the session; a user's pick never sets it).
let editBadgeSuppressed = false;
let svelteComponentSession = null; let svelteComponentSession = null;
let svelteRuntimePromise = null; let svelteRuntimePromise = null;
let pendingSvelteComponentRetryObserver = null; let pendingSvelteComponentRetryObserver = null;
@@ -983,9 +991,20 @@
} }
} catch { /* cross-origin */ } } catch { /* cross-origin */ }
} }
// The selector a mechanical bake would anchor lasting rules on, and how
// many elements it matches right now: the bake refuses anything but one,
// since its rules would restyle every match, not just this element.
const cssIdent = (s) => /^[A-Za-z_-][\w-]*$/.test(s);
const anchorClasses = [...el.classList].filter(cssIdent);
const anchor = el.id && cssIdent(el.id)
? '#' + el.id
: (anchorClasses.length ? el.tagName.toLowerCase() + '.' + anchorClasses.join('.') : null);
let anchorMatches = null;
if (anchor) { try { anchorMatches = document.querySelectorAll(anchor).length; } catch { anchorMatches = null; } }
return { return {
tagName: el.tagName.toLowerCase(), id: el.id || null, tagName: el.tagName.toLowerCase(), id: el.id || null,
classes: [...el.classList], classes: [...el.classList],
anchor, anchorMatches,
textContent: (el.textContent || '').slice(0, 500), textContent: (el.textContent || '').slice(0, 500),
outerHTML: sanitizedContextOuterHTML(el, 10000), outerHTML: sanitizedContextOuterHTML(el, 10000),
computedStyles: { computedStyles: {
@@ -2037,6 +2056,7 @@
function setLiveState(next) { function setLiveState(next) {
state = next; state = next;
window.__IMPECCABLE_LIVE_STATE__ = next; window.__IMPECCABLE_LIVE_STATE__ = next;
retryDeclinedAgentTargets();
syncPageInteractionCursor(); syncPageInteractionCursor();
// Whether a queued steer is still behind a generation is a function of this // Whether a queued steer is still behind a generation is a function of this
// state, so the hint has to move with it, not only with the 5s poll. // state, so the hint has to move with it, not only with the 5s poll.
@@ -4014,6 +4034,7 @@
function hidePendingApplyDock() { function hidePendingApplyDock() {
pendingApplyInFlight = false; pendingApplyInFlight = false;
retryDeclinedAgentTargets();
clearStoredManualApplyState(); clearStoredManualApplyState();
if (pendingIntroAnimation) { pendingIntroAnimation.cancel(); pendingIntroAnimation = null; } if (pendingIntroAnimation) { pendingIntroAnimation.cancel(); pendingIntroAnimation = null; }
if (pendingDockEl) pendingDockEl.style.display = 'none'; if (pendingDockEl) pendingDockEl.style.display = 'none';
@@ -4047,6 +4068,7 @@
function setPendingApplyLoading(loading, count) { function setPendingApplyLoading(loading, count) {
if (!pendingPillEl || !pendingPillLabelEl || !pendingPillCountEl || !pendingTrashBtn) return; if (!pendingPillEl || !pendingPillLabelEl || !pendingPillCountEl || !pendingTrashBtn) return;
pendingApplyInFlight = loading === true; pendingApplyInFlight = loading === true;
if (!pendingApplyInFlight) retryDeclinedAgentTargets();
const currentCount = count || parseInt(pendingPillEl.dataset.count || '0', 10) || 0; const currentCount = count || parseInt(pendingPillEl.dataset.count || '0', 10) || 0;
if (pendingApplyInFlight) storeManualApplyState(currentCount); if (pendingApplyInFlight) storeManualApplyState(currentCount);
else clearStoredManualApplyState(); else clearStoredManualApplyState();
@@ -4688,6 +4710,7 @@
} }
function renderEditBadge(mode) { function renderEditBadge(mode) {
if (editBadgeSuppressed || sessionOrigin === 'agent') mode = 'hidden';
if (mode === 'hidden' || !editBadgeEl) { if (mode === 'hidden' || !editBadgeEl) {
hideConfigureBarTooltip(); hideConfigureBarTooltip();
if (editBadgeEl) editBadgeEl.style.display = 'none'; if (editBadgeEl) editBadgeEl.style.display = 'none';
@@ -6181,6 +6204,8 @@
resetSessionFileMeta(); resetSessionFileMeta();
currentSessionId = null; currentSessionId = null;
parameterGenerationState = 'idle'; parameterGenerationState = 'idle';
sessionOrigin = null;
editBadgeSuppressed = false;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
expectedVariants = 0; expectedVariants = 0;
arrivedVariants = 0; arrivedVariants = 0;
@@ -7112,6 +7137,398 @@
} }
// //
// ------------------------------------------------------------------
// Agent-initiated targeting (the `generate` command). The agent names an
// element by CSS selector over POST /agent-target; the server pushes an
// `agent_target` SSE message here. The overlay resolves the selector,
// scrolls the element into view, enters the same picked state a user
// click produces, and fires the normal Go pipeline, so everything
// downstream (generate event, variants, cycling, accept) is unchanged.
// The verdict goes back through POST /agent-target-result, which resolves
// the agent's held-open CLI call.
function postAgentTargetResult(targetId, result) {
fetch('http://localhost:' + PORT + '/agent-target-result?token=' + TOKEN, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: TOKEN, targetId, clientId: AGENT_TARGET_CLIENT_ID, ...result }),
}).catch(() => { /* server gone; nothing to report to */ });
}
function describeAgentTargetCandidate(el) {
return {
tag: el.tagName.toLowerCase(),
id: el.id || null,
classes: [...el.classList].filter((c) => !c.startsWith('impeccable-')),
text: (el.textContent || '').trim().slice(0, 80),
};
}
function resolveAgentTargetElement(msg) {
let matched;
try {
matched = [...document.querySelectorAll(msg.selector)];
} catch {
return { error: { ok: false, error: 'invalid_selector', selector: msg.selector } };
}
let candidates = matched.filter((el) => pickable(el));
if (msg.text) {
const needle = String(msg.text).toLowerCase();
candidates = candidates.filter((el) => (el.textContent || '').toLowerCase().includes(needle));
}
if (candidates.length === 0) {
return {
error: {
ok: false,
error: 'no_match',
selector: msg.selector,
matchCount: 0,
// How many nodes the raw selector hit before the pickable/text
// filters: distinguishes a wrong selector from an unpickable match.
rawMatchCount: matched.length,
},
};
}
if (Number.isInteger(msg.index)) {
const el = candidates[msg.index - 1];
if (!el) {
return { error: { ok: false, error: 'index_out_of_range', selector: msg.selector, matchCount: candidates.length } };
}
return { el, matchCount: candidates.length };
}
if (candidates.length > 1) {
return {
error: {
ok: false,
error: 'ambiguous',
selector: msg.selector,
matchCount: candidates.length,
candidates: candidates.slice(0, 8).map(describeAgentTargetCandidate),
},
};
}
return { el: candidates[0], matchCount: 1 };
}
function scrollAgentTargetIntoView(el, done) {
const rect = el.getBoundingClientRect();
if (rect.top >= 0 && rect.bottom <= window.innerHeight) { done(); return; }
let settled = false;
let fallback = null;
const finish = () => {
if (settled) return;
settled = true;
removeEventListener('scrollend', finish, true);
if (fallback) clearTimeout(fallback);
done();
};
// scrollend where supported; a timer covers engines without it and the
// no-movement case (element already at its final resting position).
addEventListener('scrollend', finish, true);
fallback = setTimeout(finish, 1200);
el.scrollIntoView({ block: 'center', behavior: 'smooth' });
}
// One id per page load: the server keys claims and roll-call reports on
// it, and only the tab that holds the lease can renew it.
const AGENT_TARGET_CLIENT_ID = id8();
// The agent target an agent-initiated Go is serving: set by
// actOnAgentTarget around its handleGo call, read once by handleGo.
let agentTargetForGo = null;
// The helper's word on its global bar. The generate lane asks the helper
// to keep it out of the way (`impeccable live --no-live-bar`, or an agent
// target carrying hideLiveBar), and the helper tells every connected tab
// at once (`live_bar`) and every later connection on `connected`, so the
// bar stays hidden in every tab, through reloads, the accept, and the
// bake, until the helper stops and takes the overlay with it. The variant
// controls still show.
let liveBarHiddenByHelper = false;
function applyLiveBarPreference(hidden) {
liveBarHiddenByHelper = hidden === true;
setLiveBarHidden(liveBarHiddenByHelper);
}
// A plain live session must never notice this code: hiding remembers the
// bar's own display value and restoring puts exactly that back, and a
// restore on a bar that is not hidden is a no-op, so the `connected`
// frame every session receives changes nothing unless the lane asked.
function setLiveBarHidden(hidden) {
if (!globalBarEl) return;
if (hidden) {
if (globalBarEl.style.display !== 'none') {
globalBarEl.dataset.liveBarDisplay = globalBarEl.style.display || 'flex';
globalBarEl.style.display = 'none';
}
return;
}
if (globalBarEl.style.display === 'none') {
globalBarEl.style.display = globalBarEl.dataset.liveBarDisplay || 'flex';
}
}
function claimAgentTarget(targetId, report) {
return fetch('http://localhost:' + PORT + '/agent-target-claim?token=' + TOKEN, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: TOKEN, targetId, clientId: AGENT_TARGET_CLIENT_ID, ...report }),
}).then((res) => res.json())
.then((j) => ({ granted: !!j && j.granted === true, pending: !!j && j.pending === true }))
.catch(() => ({ granted: false, pending: false }));
}
// `exceptTargetId` is the target this call is about: a tab acting on it
// is not busy for itself, but it is busy for every other target, or two
// held requests could both be claimed here and the second Go would
// overwrite the session the first one minted.
function agentTargetBusyReason(exceptTargetId) {
if (pendingApplyInFlight) return 'manual_apply_in_flight';
if (state !== 'IDLE' && state !== 'PICKING' && state !== 'CONFIGURING') return 'session_active';
for (const [targetId, status] of agentTargetsSeen) {
if (status === 'acting' && targetId !== exceptTargetId) return 'agent_target_in_flight';
}
return null;
}
// Targets this tab declined as busy. A busy report is only this tab's word
// at that moment: the moment it is free again (setLiveState), it claims
// each of these as eligible, and the server drops the stale report, so a
// busy verdict is never built on a tab that has since gone idle. The
// server denies claims for resolved targets, so retries are harmless.
const busyDeclinedTargets = new Map();
function declineAgentTargetBusy(msg, busy) {
busyDeclinedTargets.set(msg.targetId, msg);
noteAgentTarget(msg.targetId, 'declined');
claimAgentTarget(msg.targetId, { eligible: false, state, reason: busy });
}
// A torn-down overlay, or one whose helper connection is gone, cannot
// serve a target and must not even claim one: it would hold the lease for
// a request it will never act on.
function agentTargetOverlayGone() {
return !evtSource;
}
// A denied claimant retries at this cadence, a little over the lease, so
// the first retry after a dead holder's lease lapses is granted.
const AGENT_TARGET_RESCUE_RETRY_MS = 3500;
// Claim the lease and act as the holder. A denied claim means another tab
// holds the lease. That holder can die before posting its result (reload,
// crash, even after renewing), and its lease lapses after ~3s, so this tab
// keeps retrying for as long as the server still holds the request: the
// answer's `pending` is the server's word that the request is alive, and
// it turns false the moment the request resolved or timed out, so no tab
// retries a request nobody awaits. A tab that turned busy meanwhile joins
// the roll call instead of taking a lease it cannot use. The first claim
// and the busy-to-idle re-claim share this.
function claimAndActOnAgentTarget(msg) {
if (agentTargetOverlayGone()) return;
const busy = agentTargetBusyReason(msg.targetId);
if (busy) { declineAgentTargetBusy(msg, busy); return; }
if (declineAgentTargetUnresolvable(msg)) return;
claimAgentTarget(msg.targetId, { eligible: true }).then((claim) => {
if (claim.granted) { noteAgentTarget(msg.targetId, 'acting'); actOnAgentTarget(msg); return; }
noteAgentTarget(msg.targetId, 'denied');
if (!claim.pending) return;
setTimeout(() => claimAndActOnAgentTarget(msg), AGENT_TARGET_RESCUE_RETRY_MS);
});
}
function retryDeclinedAgentTargets() {
if (busyDeclinedTargets.size === 0 || agentTargetBusyReason()) return;
for (const [targetId, msg] of busyDeclinedTargets) {
busyDeclinedTargets.delete(targetId);
claimAndActOnAgentTarget(msg);
}
}
// This page's participation in each target it heard: 'acting' once a
// claim was granted, 'done' once it replied (or stood down from a lapsed
// lease), else the word it last gave. The server replays pending targets
// to every connection that opens. After a reconnect that overlapped the
// old connection the server still holds this page's word; after one that
// did not, it dropped the word on the close, so a replayed target is
// handled again: a busy or unresolvable page re-declines (idempotent), an
// idle page claims.
const agentTargetsSeen = new Map();
function noteAgentTarget(targetId, status) {
agentTargetsSeen.set(targetId, status);
if (agentTargetsSeen.size > 100) agentTargetsSeen.delete(agentTargetsSeen.keys().next().value);
}
// A target this page took a lease on is off-limits for a replay: while
// acting (a second claim or Go), and once done, because its result may
// still be on the wire and this tab is GENERATING by then, so handling
// the replay would decline busy, hand the lease back mid-resolution, and
// let another tab fire a second Go.
function agentTargetTaken(targetId) {
const status = agentTargetsSeen.get(targetId);
return status === 'acting' || status === 'done';
}
// Only a page that can resolve the target claims it. A tab whose page
// lacks the element declines with its resolution verdict instead, so a
// first-wins claim never lets the wrong page answer for a target that
// another page has. The server prefers a busy report (a tab that could
// serve later) over these, and returns the resolution verdict only when
// no connected page can serve.
//
// An element can be momentarily absent (a route still rendering, an HMR
// commit mid-swap), so a failed resolution is not this page's final word:
// it is re-checked a few times over about two seconds, claiming the
// moment the element mounts, and only the last miss is reported. The
// server's timeout still bounds the whole exchange.
// The page reports the miss at once (so the other overlays' words can
// complete the roll call) and keeps re-checking at this cadence for as
// long as the server says the request is pending: the server holds an
// all-no_match roll call open for a short grace precisely so a late mount
// can still be claimed, drops the stale report on an eligible claim, and
// ends the watch by answering pending:false once the request resolved or
// timed out.
const AGENT_TARGET_RESOLVE_WATCH_MS = 500;
function declineAgentTargetUnresolvable(msg) {
const probe = resolveAgentTargetElement(msg);
if (!probe.error) return false;
reportAgentTargetUnresolvable(msg, probe.error);
return true;
}
function reportAgentTargetUnresolvable(msg, error) {
noteAgentTarget(msg.targetId, 'declined');
claimAgentTarget(msg.targetId, { eligible: false, state, reason: 'no_match', result: error }).then((answer) => {
if (!answer.pending) return;
setTimeout(() => watchAgentTargetResolution(msg, error), AGENT_TARGET_RESOLVE_WATCH_MS);
});
}
function watchAgentTargetResolution(msg, lastError) {
if (agentTargetOverlayGone() || agentTargetTaken(msg.targetId)) return;
const busy = agentTargetBusyReason(msg.targetId);
if (busy) { declineAgentTargetBusy(msg, busy); return; }
const probe = resolveAgentTargetElement(msg);
if (!probe.error) { claimAndActOnAgentTarget(msg); return; }
// Still unresolvable: re-report (idempotent); the answer says whether
// the server is still holding the request open.
reportAgentTargetUnresolvable(msg, probe.error || lastError);
}
function handleAgentTarget(msg) {
if (!msg || typeof msg.targetId !== 'string') return;
if (agentTargetTaken(msg.targetId)) return;
noteAgentTarget(msg.targetId, 'heard');
const busy = agentTargetBusyReason(msg.targetId);
if (busy) {
// Roll call: a busy tab reports itself and never acts. The server
// answers `busy` the moment every connected overlay has reported, so
// an idle tab elsewhere is never raced by a timer.
declineAgentTargetBusy(msg, busy);
return;
}
if (declineAgentTargetUnresolvable(msg)) return;
// Eligible tabs race for the server's lease and only the holder acts. A
// hidden tab yields a short head start so a visible one wins when both
// exist, and still serves the request on its own: the user finds the
// selection waiting when they return to it.
setTimeout(() => claimAndActOnAgentTarget(msg), document.hidden ? 150 : 0);
}
function actOnAgentTarget(msg) {
if (agentTargetOverlayGone()) return;
// Every exit ends this tab's acting state, so a later target is not
// refused for a Go that already happened or never will.
const reply = (result) => { noteAgentTarget(msg.targetId, 'done'); postAgentTargetResult(msg.targetId, result); };
const busy = agentTargetBusyReason(msg.targetId);
if (busy) {
// Turned busy between claim and act: report it, which also hands the
// lease back so the roll call can complete or a rescuer can claim.
declineAgentTargetBusy(msg, busy);
return;
}
const resolved = resolveAgentTargetElement(msg);
if (resolved.error) {
// The element went away between claim and act. A result would end the
// request for every tab; a decline hands the lease back so another
// page or a remount can still serve it.
reportAgentTargetUnresolvable(msg, resolved.error);
return;
}
const el = resolved.el;
if (msg.dryRun) {
reply({
ok: true,
dryRun: true,
matchCount: resolved.matchCount,
element: describeAgentTargetCandidate(el),
});
return;
}
scrollAgentTargetIntoView(el, () => {
// Torn down during the scroll settle: do not renew. The lease lapses
// for a rescuer instead of Go minting a session on a dismantled
// overlay.
if (agentTargetOverlayGone()) return;
// Renew the lease right before the irreversible part: a tab whose
// lease lapsed while it scrolled (a rescuer took over) stops here, so
// one request never gets two Go presses.
claimAgentTarget(msg.targetId, { eligible: true }).then((renewal) => {
if (!renewal.granted) { noteAgentTarget(msg.targetId, 'done'); return; }
// An insert placement left mid-configure gives way, exactly as a
// click outside it does in handleClick.
if (state === 'CONFIGURING' && configureKind === 'insert') cancelInsertConfigure();
// Mirror of the user-click pick entry in handleClick, minus the
// pick-mode gate (the agent's intent replaces the toggle); the entry
// goes through beginNewLiveConfiguration like every other pick so
// deferred recovery sees a fresh interaction revision.
selectedElement = el;
beginNewLiveConfiguration();
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
showBar('configure');
editBadgeSuppressed = true;
renderEditBadge('hidden');
startScrollTracking();
maybePrefetchPage();
maybeWarnConditionalAncestor(selectedElement);
// Preset what the agent asked for, then fire the same Go a user press
// fires. handleGo reads exactly these inputs.
selectedAction = msg.action;
selectedCount = msg.count;
// updateBarContent rebuilds the configure row and replaces the input
// element, so the prompt must be written into the input it creates,
// never before (the action-chip click handler does the same dance).
updateBarContent('configure');
const input = uiGetById(PREFIX + '-input');
if (input) input.value = msg.prompt || '';
// The target rides on the generate event too: the helper resolves
// the request from whichever lands first, so a page that dies
// between Go and its result cannot leave the request pending for a
// second Go elsewhere.
const candidate = describeAgentTargetCandidate(el);
agentTargetForGo = { targetId: msg.targetId, matchCount: resolved.matchCount, action: msg.action, count: msg.count, element: candidate };
handleGo();
agentTargetForGo = null;
if (state === 'GENERATING' && currentSessionId) {
reply({
ok: true,
matchCount: resolved.matchCount,
sessionId: currentSessionId,
action: msg.action,
count: msg.count,
element: candidate,
});
} else {
reply({ ok: false, error: 'go_failed', state });
}
});
});
}
// SSE (server→browser) + fetch POST (browser→server) // SSE (server→browser) + fetch POST (browser→server)
// Zero-dependency replacement for WebSocket. // Zero-dependency replacement for WebSocket.
// //
@@ -7121,7 +7538,7 @@
const SSE_MAX_RETRIES = 20; // generous: heartbeats keep the connection alive, so retries mean real trouble const SSE_MAX_RETRIES = 20; // generous: heartbeats keep the connection alive, so retries mean real trouble
function connectSSE() { function connectSSE() {
evtSource = new EventSource('http://localhost:' + PORT + '/events?token=' + TOKEN); evtSource = new EventSource('http://localhost:' + PORT + '/events?token=' + TOKEN + '&clientId=' + AGENT_TARGET_CLIENT_ID);
evtSource.onopen = () => { evtSource.onopen = () => {
sseRetries = 0; // reset on successful (re)connect sseRetries = 0; // reset on successful (re)connect
@@ -7132,8 +7549,11 @@
let msg; try { msg = JSON.parse(e.data); } catch { return; } let msg; try { msg = JSON.parse(e.data); } catch { return; }
switch (msg.type) { switch (msg.type) {
case 'connected': case 'connected':
applyLiveBarPreference(msg.hideLiveBar === true);
hasProjectContext = !!msg.hasProjectContext; hasProjectContext = !!msg.hasProjectContext;
if (!hasProjectContext) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000); // The generate lane runs without PRODUCT.md by design and never
// sends the user to init, so its quiet chrome skips this notice.
if (!hasProjectContext && !liveBarHiddenByHelper) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000);
console.log('[impeccable] Live mode connected.'); console.log('[impeccable] Live mode connected.');
syncAgentPollingUi(!!msg.agentPolling); syncAgentPollingUi(!!msg.agentPolling);
startAgentStatusPoll(); startAgentStatusPoll();
@@ -7143,9 +7563,15 @@
syncPageInteractionCursor(); syncPageInteractionCursor();
syncPageChatFocus('sse-connected'); syncPageChatFocus('sse-connected');
break; break;
case 'live_bar':
applyLiveBarPreference(msg.hidden === true);
break;
case 'agent_polling': case 'agent_polling':
syncAgentPollingUi(!!msg.connected); syncAgentPollingUi(!!msg.connected);
break; break;
case 'agent_target':
handleAgentTarget(msg);
break;
case 'agent_phase': case 'agent_phase':
if (msg.id === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) { if (msg.id === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) {
// Advance the visible phase monotonically. A behind/resumed // Advance the visible phase monotonically. A behind/resumed
@@ -7208,6 +7634,11 @@
disableInlineEdit(); disableInlineEdit();
refreshParamsPanel(); refreshParamsPanel();
} }
// The done reply is the agent's last word on this generation:
// with every variant mounted and no knobs declared, the Tune
// chip must stop spinning. A reload between the mount and this
// reply restored the pending state from the cache.
completeParameterGenerationIfReady();
break; break;
} }
// Source fallback when HMR did not land variants in this tab. // Source fallback when HMR did not land variants in this tab.
@@ -7371,6 +7802,15 @@
}).then(async res => { }).then(async res => {
if (res.ok) return res; if (res.ok) return res;
const body = await res.json().catch(() => ({})); const body = await res.json().catch(() => ({}));
// The helper refused to open a session for an agent target it has
// already answered (another page served it after this page's lease
// lapsed mid-capture, or the request timed out): drop the local
// session and hand the surface back.
if (body.error === 'agent_target_already_served' && msg.type === 'generate'
&& msg.id && msg.id === currentSessionId) {
abandonSupersededGo(msg.id);
return null;
}
// The server refused to journal progress for a session it has never // The server refused to journal progress for a session it has never
// seen: this browser is carrying state from another project or a // seen: this browser is carrying state from another project or a
// wiped store (two apps sharing a localhost port). Continuing to // wiped store (two apps sharing a localhost port). Continuing to
@@ -7392,6 +7832,14 @@
return sessionCreationGate.then(doSend); return sessionCreationGate.then(doSend);
} }
function abandonSupersededGo(sessionId) {
if (sessionId !== currentSessionId) return;
console.warn('[impeccable] The helper already answered this agent target; clearing session ' + sessionId + '.');
markSessionHandled();
cleanup({ instantChrome: true });
showToast('The helper already answered this request, so this session was cleared. Pick an element to start fresh.', 6000);
}
let abandonedForeignSessionId = null; let abandonedForeignSessionId = null;
function abandonForeignSession(sessionId) { function abandonForeignSession(sessionId) {
if (abandonedForeignSessionId === sessionId || sessionId !== currentSessionId) return; if (abandonedForeignSessionId === sessionId || sessionId !== currentSessionId) return;
@@ -7796,6 +8244,7 @@
visibleVariant = 0; visibleVariant = 0;
generationPhase = 'queued'; generationPhase = 'queued';
parameterGenerationState = 'pending'; parameterGenerationState = 'pending';
sessionOrigin = agentTargetForGo ? 'agent' : null;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
resetSessionFileMeta(); resetSessionFileMeta();
@@ -7821,6 +8270,24 @@
}; };
if (snapshot.comments.length > 0) basePayload.comments = snapshot.comments; if (snapshot.comments.length > 0) basePayload.comments = snapshot.comments;
if (snapshot.strokes.length > 0) basePayload.strokes = snapshot.strokes; if (snapshot.strokes.length > 0) basePayload.strokes = snapshot.strokes;
if (agentTargetForGo) {
// An agent-initiated Go names the target it serves (see
// actOnAgentTarget): the helper resolves that request from this event
// as well as from the overlay's own result post.
basePayload.agentTarget = {
targetId: agentTargetForGo.targetId,
clientId: AGENT_TARGET_CLIENT_ID,
result: {
ok: true,
matchCount: agentTargetForGo.matchCount,
sessionId: currentSessionId,
action: agentTargetForGo.action,
count: agentTargetForGo.count,
element: agentTargetForGo.element,
},
};
agentTargetForGo = null;
}
// Hide the interactive overlay so it doesn't linger during generation. // Hide the interactive overlay so it doesn't linger during generation.
hideAnnotOverlay(); hideAnnotOverlay();
@@ -7881,6 +8348,7 @@
visibleVariant = 0; visibleVariant = 0;
generationPhase = 'queued'; generationPhase = 'queued';
parameterGenerationState = 'pending'; parameterGenerationState = 'pending';
sessionOrigin = agentTargetForGo ? 'agent' : null;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
resetSessionFileMeta(); resetSessionFileMeta();
selectedElement = placeholderElement; selectedElement = placeholderElement;
@@ -8927,6 +9395,8 @@ void main() {
pagePickSkipClick = false; pagePickSkipClick = false;
currentSessionId = null; currentSessionId = null;
parameterGenerationState = 'idle'; parameterGenerationState = 'idle';
sessionOrigin = null;
editBadgeSuppressed = false;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
selectedAction = 'impeccable'; selectedAction = 'impeccable';
pendingAcceptedSession = null; pendingAcceptedSession = null;
@@ -9018,6 +9488,7 @@ void main() {
paramsCurrentValues = { ...saved.paramValues }; paramsCurrentValues = { ...saved.paramValues };
} }
if (saved.parameterState) parameterGenerationState = saved.parameterState; if (saved.parameterState) parameterGenerationState = saved.parameterState;
sessionOrigin = saved.origin === 'agent' ? 'agent' : null;
if (saved.generationPhase) generationPhase = saved.generationPhase; if (saved.generationPhase) generationPhase = saved.generationPhase;
} }
@@ -9105,7 +9576,12 @@ void main() {
} }
function restoreSessionWithoutWrapper(reason, activeSessions) { function restoreSessionWithoutWrapper(reason, activeSessions) {
const cached = loadSession(); // The session cache is per origin, so a tab on another page of the same
// app sees this page's session too. Only the page that saved it may
// resume it: the server-adoption branch below already applies the same
// check, and a tab on another page has nothing to render for it.
const cachedRaw = loadSession();
const cached = cachedRaw?.id && !pageMatchesCurrent(cachedRaw.pageUrl) ? null : cachedRaw;
// localStorage is a cache, not a gate. A cleared tab, a second browser // localStorage is a cache, not a gate. A cleared tab, a second browser
// profile, or a teardown that dropped local state all leave the durable // profile, or a teardown that dropped local state all leave the durable
// server session as the only record of work in progress; adopt it instead // server session as the only record of work in progress; adopt it instead
@@ -9218,6 +9694,7 @@ void main() {
pageUrl: location.pathname, pageUrl: location.pathname,
paramValues: { ...paramsCurrentValues }, paramValues: { ...paramsCurrentValues },
parameterState: parameterGenerationState, parameterState: parameterGenerationState,
origin: sessionOrigin || undefined,
insertPlaceholder: insertPlaceholderSnapshot || undefined, insertPlaceholder: insertPlaceholderSnapshot || undefined,
pickedAnchor: pickedAnchorSnapshot || undefined, pickedAnchor: pickedAnchorSnapshot || undefined,
pickedAnchorViewportTop: Number.isFinite(pickedAnchorViewportTop) ? pickedAnchorViewportTop : undefined, pickedAnchorViewportTop: Number.isFinite(pickedAnchorViewportTop) ? pickedAnchorViewportTop : undefined,
@@ -9343,6 +9820,8 @@ void main() {
pagePickSkipClick = false; pagePickSkipClick = false;
currentSessionId = null; currentSessionId = null;
parameterGenerationState = 'idle'; parameterGenerationState = 'idle';
sessionOrigin = null;
editBadgeSuppressed = false;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
selectedAction = 'impeccable'; selectedAction = 'impeccable';
renderEditBadge('hidden'); renderEditBadge('hidden');
@@ -9601,6 +10080,14 @@ void main() {
const resumedState = arrivedVariants > 0 ? 'CYCLING' : 'GENERATING'; const resumedState = arrivedVariants > 0 ? 'CYCLING' : 'GENERATING';
// A reload between the variants mounting and the agent's done reply
// restores a pending Tune state from the cache; the helper knows whether
// that generation already finished.
if (arrivedVariants >= expectedVariants && expectedVariants > 0
&& (parameterGenerationState === 'pending' || parameterGenerationState === 'loading')) {
settleParameterStateFromHelper(sessionId);
}
// Find the visible variant's content element for highlight positioning. // Find the visible variant's content element for highlight positioning.
const isInsert = wrapper.dataset.impeccableMode === 'insert'; const isInsert = wrapper.dataset.impeccableMode === 'insert';
const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null; const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null;
@@ -11065,6 +11552,21 @@ void main() {
} }
} }
// After a resume the cache may say the Tune knobs are still coming while
// the agent already replied done before the reload. The helper's session
// record settles it; otherwise the done reply on SSE does.
function settleParameterStateFromHelper(sessionId) {
fetch('http://localhost:' + PORT + '/status?token=' + TOKEN, { cache: 'no-store' })
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
if (!data || sessionId !== currentSessionId) return;
const session = (data.activeSessions || []).find((s) => s && s.id === sessionId);
if (!session) return;
if (session.generationCompletedAt || session.generationPhase === 'completed') completeParameterGenerationIfReady();
})
.catch(() => { /* the done reply on SSE settles it otherwise */ });
}
function fetchAgentPollingStatus() { function fetchAgentPollingStatus() {
fetch('http://localhost:' + PORT + '/status?token=' + TOKEN, { cache: 'no-store' }) fetch('http://localhost:' + PORT + '/status?token=' + TOKEN, { cache: 'no-store' })
.then((res) => (res.ok ? res.json() : null)) .then((res) => (res.ok ? res.json() : null))
@@ -11104,11 +11606,15 @@ void main() {
uiAppendStyle(s); uiAppendStyle(s);
} }
// The generate lane's helper says so in the served script itself, so a
// lane session never draws the bar at all; every other session mounts
// it exactly as before.
const barHiddenFromStart = window.__IMPECCABLE_LIVE_BAR_HIDDEN__ === true;
globalBarEl = el('div', { globalBarEl = el('div', {
position: 'fixed', bottom: '14px', left: '50%', position: 'fixed', bottom: '14px', left: '50%',
transform: 'translateX(-50%) translateY(20px)', transform: 'translateX(-50%) translateY(20px)',
zIndex: Z.bar + 5, zIndex: Z.bar + 5,
display: 'flex', alignItems: 'stretch', display: barHiddenFromStart ? 'none' : 'flex', alignItems: 'stretch',
gap: '0', gap: '0',
width: 'max-content', width: 'max-content',
background: P.surface, background: P.surface,
@@ -11124,6 +11630,10 @@ void main() {
}); });
globalBarEl.id = PREFIX + '-global-bar'; globalBarEl.id = PREFIX + '-global-bar';
globalBarEl.dataset.theme = theme; globalBarEl.dataset.theme = theme;
if (barHiddenFromStart) {
liveBarHiddenByHelper = true;
globalBarEl.dataset.liveBarDisplay = 'flex';
}
// Brand mark - kinpaku Impeccable icon (site header / favicon paths). // Brand mark - kinpaku Impeccable icon (site header / favicon paths).
const brand = el('span', { const brand = el('span', {
@@ -11519,6 +12029,9 @@ void main() {
// Listen for detection results AND ready signal // Listen for detection results AND ready signal
window.addEventListener('message', onDetectMessage); window.addEventListener('message', onDetectMessage);
updateGlobalBarState(); updateGlobalBarState();
// The helper may already have said the bar stays hidden (a connect
// that raced the bar build, or a reload mid-lane): re-apply it here.
if (liveBarHiddenByHelper) setLiveBarHidden(true);
} }
function updateGlobalBarState() { function updateGlobalBarState() {
@@ -11715,6 +12228,13 @@ void main() {
/** Full teardown: remove all UI, disconnect SSE, clean up. */ /** Full teardown: remove all UI, disconnect SSE, clean up. */
function teardown() { function teardown() {
// Declined targets die with the overlay: the IDLE transition below must
// not re-claim a lease this page can no longer act on. So does the
// target ledger: an 'acting' entry from a Go that never happened must
// not refuse every target the next connection hears.
busyDeclinedTargets.clear();
agentTargetsSeen.clear();
liveBarHiddenByHelper = false;
stopAgentStatusPoll(); stopAgentStatusPoll();
hideAgentPollTooltip(); hideAgentPollTooltip();
if (agentPollTooltipEl) { if (agentPollTooltipEl) {
+3 -2
View File
@@ -1,7 +1,7 @@
--- ---
name: impeccable name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks. description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.4.0 version: 4.3.1
user-invocable: true user-invocable: true
license: Apache 2.0 license: Apache 2.0
--- ---
@@ -64,7 +64,8 @@ Choose the mode from the requested surface, not the product, and persist it only
| `clarify [target]` | Fix | Improve UX copy, labels, and error messages | [reference/clarify.md](reference/clarify.md) | | `clarify [target]` | Fix | Improve UX copy, labels, and error messages | [reference/clarify.md](reference/clarify.md) |
| `adapt [target]` | Fix | Adapt for different devices and screen sizes | [reference/adapt.md](reference/adapt.md) · native: [reference/adapt.native.md](reference/adapt.native.md) | | `adapt [target]` | Fix | Adapt for different devices and screen sizes | [reference/adapt.md](reference/adapt.md) · native: [reference/adapt.native.md](reference/adapt.native.md) |
| `optimize [target]` | Fix | Diagnose and fix UI performance | [reference/optimize.md](reference/optimize.md) | | `optimize [target]` | Fix | Diagnose and fix UI performance | [reference/optimize.md](reference/optimize.md) |
| `live` | Iterate | Visual variant mode: pick elements in the browser, generate alternatives | [reference/live.md](reference/live.md) | | `live` | Iterate | Visual variant mode: pick elements in the browser, iterate on alternatives | [reference/live.md](reference/live.md) |
| `generate [n] [action] [element]` | Iterate | Variants, versions, or alternatives of a named element to choose from in the live browser; no manual picking | [reference/generate.md](reference/generate.md) |
Routing: Routing:
@@ -188,6 +188,12 @@ Test thoroughly across contexts:
- **Edge cases**: Very small screens (320px), very large screens (4K) - **Edge cases**: Very small screens (320px), very large screens (4K)
- **Slow connections**: Test on throttled network - **Slow connections**: Test on throttled network
**Custom controls** (sliders, drag surfaces, scrollable control strips): a before/after slider can pass every width check above and still refuse to drag on iOS, so exercise each one in scope in the same batched round as the checks above:
- **Primary gesture**: Tap it and confirm it responds as designed, then drag it with the target input method; the drag must complete, not just start
- **Scroll across it**: A swipe along the page's scroll axis across the control scrolls the page or container without activating it; a drag that starts on the control along its axis moves the control, not the page. Neither failure throws an error, so try both
- **Evidence**: Say what produced the evidence: an emulated viewport, synthesized touch input through a browser tool, which engine ran it (Chromium is not Safari), or a physical device. Screenshots and resized viewports verify layout, never a gesture. Name what stayed untested and move on; unreachable hardware is a reported gap, not a blocker
When the adaptation feels native to each context, hand off to `/impeccable polish` for the final pass. When the adaptation feels native to each context, hand off to `/impeccable polish` for the final pass.
--- ---
+2 -1
View File
@@ -48,11 +48,12 @@ Run comprehensive checks across 5 dimensions. Score each dimension 0-4 using the
**Check for**: **Check for**:
- **Fixed widths**: Hard-coded widths that break on mobile - **Fixed widths**: Hard-coded widths that break on mobile
- **Touch targets**: Interactive elements < 44x44px - **Touch targets**: Interactive elements < 44x44px
- **Broken touch interaction**: Custom sliders, drag surfaces, and scrollable control strips whose primary gesture fails under touch, that swallow page scroll or lose the drag to it, or that stay stuck after an interrupted gesture. Code tells: mouse-only handlers, no `touch-action` on a pointer-event drag surface, drag state that nothing clears on cancel, lost capture, or blur. Exercise the gesture when a browser tool can synthesize touch (a rendered viewport proves layout, not the gesture), then say what produced the evidence (emulated viewport, synthesized touch, which engine, physical device) and what stayed untested
- **Horizontal scroll**: Content overflow on narrow viewports - **Horizontal scroll**: Content overflow on narrow viewports
- **Text scaling**: Layouts that break when text size increases - **Text scaling**: Layouts that break when text size increases
- **Missing breakpoints**: No mobile/tablet variants - **Missing breakpoints**: No mobile/tablet variants
**Score 0-4**: 0=Desktop-only (breaks on mobile), 1=Major issues (some breakpoints, many failures), 2=Partial (works on mobile, rough edges), 3=Good (responsive, minor touch target or overflow issues), 4=Excellent (fluid, all viewports, proper touch targets) **Score 0-4**: 0=Desktop-only (breaks on mobile), 1=Major issues (some breakpoints, many failures), 2=Partial (works on mobile, rough edges), 3=Good (responsive, minor touch target or overflow issues), 4=Excellent (fluid, all viewports, proper touch targets, gestures work under touch)
### 5. Implementation Integrity (CRITICAL) ### 5. Implementation Integrity (CRITICAL)
@@ -1,57 +0,0 @@
# Component review
Use this checkpoint on comp-led builds after producing the initial component kit and before composing the page. The approved comp is the reference. The user reviews the actual produced components, including code; a list of planned assets or screenshots supplied by the builder is not a review of what will ship.
## Prepare the component kit
Keep the measured spec's region IDs. Include every visible region: produced raster assets and working HTML/CSS/SVG for text, controls, patterns, decoration and layout elements. A region rendered in code needs an actual review document, not a promise to implement it later. Use semantic HTML for content and controls. Do not flatten the page or combine unrelated regions to avoid review. Report omitted regions so the user can mark what is missing.
Write `.impeccable/review/components.json` with this manifest format:
```json
{
"schemaVersion": 1,
"id": "components",
"title": "Component review",
"stage": "components",
"comp": {"path": ".impeccable/mocks/comp-2.png", "width": 1536, "height": 1024},
"components": [
{
"id": "illustration",
"name": "Illustration",
"medium": "raster",
"box": {"x": 0.5, "y": 0.2, "w": 0.45, "h": 0.7},
"note": "Produced cutout; positioned over the page ground.",
"preview": {"kind": "image", "path": "assets/illustration.png"},
"dependencies": [".impeccable/build/spec.json"]
},
{
"id": "headline",
"name": "Headline",
"medium": "html",
"box": {"x": 0.05, "y": 0.2, "w": 0.4, "h": 0.25},
"note": "Rendered semantic heading and its typography.",
"preview": {"kind": "page", "path": ".impeccable/review/components/headline.html"},
"dependencies": [".impeccable/build/spec.json", "assets/type.woff2"]
}
]
}
```
The coordinates above only illustrate the schema. Use the approved comp's actual pixel dimensions and each measured region's normalized bounds (`x / width`, `y / height`, `w / width`, `h / height`). A code preview is rendered at the comp viewport and cropped to that component's box, so place its content at those coordinates in the review document. Include every file the document uses in `dependencies`, including linked CSS, fonts and images. The runtime also binds the measured spec for the component stage and checks its inventory. Local paths only. Static PNG, WebP and JPEG previews retain their original bytes and actual transparency; never draw a checkerboard into the asset.
Native capture supports stable HTML/CSS and inline SVG. Supply a static review state for motion and keep the implementation's real inputs. A scripted, canvas or otherwise unsupported component is a blocker to report, not permission to substitute a raster or omit it.
## Present and wait
If the harness exposes `component_review`, call it with `manifest_path` set to `.impeccable/review/components.json`. The host captures the component files, presents this same review interface and returns the user's decisions. A suspended request is waiting for the user; it is not a failed build or an approval.
Otherwise run `.dsh/skills/impeccable/scripts/impeccable component-review capture --manifest .impeccable/review/components.json`, then start `.dsh/skills/impeccable/scripts/impeccable component-review serve --session <returned session>` in the background. Open the URL it prints in the available browser and wait for the user. Read the result with `.dsh/skills/impeccable/scripts/impeccable component-review verify --manifest .impeccable/review/components.json`; pending, needs-work and stale input all refuse approval. Never submit the page or write a receipt on the user's behalf.
The user can approve components, request changes, and mark missing regions. Act on their feedback without replacing it with your own favorable verdict. Keep component IDs stable, update the actual implementation and dependency list, and present another round. The UI carries only approvals whose component inputs have not changed. Do not ask the user to reapprove unchanged work. Continue only when the inventory is confirmed and all components are approved.
## Assemble and review
Build the page from the approved component files. Replacing, simplifying or changing an approved component requires a new component review. Run the existing plates and hero gates; human review does not waive their integrity checks.
After the full page and responsive checks are complete, present a second manifest at `.impeccable/review/hero.json`, with `id` and `stage` set to `hero`. Use one page-preview component covering the assembled first viewport, its real HTML entry, and its complete dependency list. The reference stays the approved comp. Call the same host review tool (or native capture/serve/verify workflow) and obtain the user's approval before the final response. Later edits to the reviewed files require a fresh review. A component-kit approval does not approve their assembled layout.
@@ -13,10 +13,6 @@ Do not redesign. Preserve the reference's visual role, silhouette, palette, ligh
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 `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; 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; report a card too thin to brief a comp, never pad it 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 (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 keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. 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. 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 `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; 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; report a card too thin to brief a comp, never pad it 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 (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 keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. 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.
## Review handoff
After the initial production batch, return the actual files and any unresolved drift to the parent for the user-facing component review in [component-review.md](../reference/component-review.md). The parent includes rendered code regions alongside these rasters. A parent or automatic visual check is not a substitute for that human checkpoint. Apply requested repairs and preserve unchanged assets; do not self-approve them. This checkpoint does not apply to the Decision Comps job above.
## Input Contract ## Input Contract
Expect the measured spec (`.impeccable/build/spec.json`, written by `impeccable comp-spec` from the approved comp), the approved comp path, and the skill scripts path. Optionally: a subset of region ids to produce, extra prompt notes per region, and format or transparency needs. Everything else you need is in the spec: each raster region's id, kind (plate, image, texture), pixel box, sampled palette, aspect, note, and the plate path it must land on. Expect the measured spec (`.impeccable/build/spec.json`, written by `impeccable comp-spec` from the approved comp), the approved comp path, and the skill scripts path. Optionally: a subset of region ids to produce, extra prompt notes per region, and format or transparency needs. Everything else you need is in the spec: each raster region's id, kind (plate, image, texture), pixel box, sampled palette, aspect, note, and the plate path it must land on.
@@ -16,7 +16,7 @@ Expect: the original request; the confirmed user answers; the artifact path(s);
## Checks, in order ## Checks, in order
0. **Evidence.** Before any other check, verify the required captures exist and every capture is valid. Required: the platform's full viewport set (web: `desktop.png` and `mobile.png`; native: one capture per shipped device class), plus every capture the calling brief names as required, a reported user viewport (`user-<width>.png`) included. Valid: no black or blank regions, content matching what the filename claims (a visit capture showing the About section is invalid), the document top visible where the file claims a full page, dimensions that make sense for the named viewport. A required capture that is absent fails exactly like one that is malformed: a viewport nobody captured is a viewport nobody inspected, and it cannot ship. When any capture fails, the whole review changes shape: return `disposition: recapture` as the first line, then one section, `recapture`, listing each missing or invalid file and what a valid capture of it shows, and stop. Never build a matrix on malformed evidence; a verdict derived from a broken capture launders the breakage into an approval, and the parent owes you a full re-review on valid captures, not a scoring round. 0. **Evidence.** Before any other check, verify the required captures exist and every capture is valid. Required: the platform's full viewport set (web: `desktop.png` and `mobile.png`; native: one capture per shipped device class), plus every capture the calling brief names as required, a reported user viewport (`user-<width>.png`) included. Valid: no black or blank regions, content matching what the filename claims (a visit capture showing the About section is invalid), the document top visible where the file claims a full page, dimensions that make sense for the named viewport. A required capture that is absent fails exactly like one that is malformed: a viewport nobody captured is a viewport nobody inspected, and it cannot ship. When any capture fails, the whole review changes shape: return `disposition: recapture` as the first line, then one section, `recapture`, listing each missing or invalid file and what a valid capture of it shows, and stop. Never build a matrix on malformed evidence; a verdict derived from a broken capture launders the breakage into an approval, and the parent owes you a full re-review on valid captures, not a scoring round.
1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and every phase before `review` is `closed` or explicitly `skipped`; an open or failed phase is a material finding. A comp-led config with no state file, or a state whose `comps` phase is neither `closed` nor `skipped`, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. 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, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all. 1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and its `comps` (or `skipped` when a surface round locked the comp), `spec`, `plates`, and `hero` phases are `closed`; a comp-led config with no state file, or a state whose `comps` phase never closed, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. 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, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all.
2. **Fidelity.** Start from the measurement, then judge what it cannot: read `.impeccable/review/diff/final/report.json` (and hero) first; every region scored `missing` or `contradicted` is a matrix row in that state unless the paired crop under `regions/` shows the score is wrong, and you say why; a region scored `match` still gets your eye for lettering character and material, which the numbers do not measure. Then, 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; 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. Three 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; medium is part of the promise. GROUND: the page field's value and temperature against the comp's, sampled from pixels on both sides when tooling allows rather than judged from memory, and read as the net on-screen result where a texture or tile paints over the base color; a ground warmer or cooler than the comp's is contradicted however faithfully the layout matches, and drift toward the rendition prior (warm cream on light grounds, blue-black slate on dark) is the direction to hunt. With no approved comp, 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 renders) as contradicted on its face; imitation material is the single most reliable mark of machine-made design. GROUND narrows rather than lapses: with no comp to sample, a color OWN-WORLD names is the target and the same warmer-or-cooler judgment applies; when OWN-WORLD names none, there is no GROUND authority, and the review says so in place of a verdict, because a target the reviewer invents turns the check into taste. A critique-reference comp on such a build is provocation, not spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is what the image dared that the build did not, and 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. A fix that requires producing an asset says so explicitly ("produce: <region> 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. 2. **Fidelity.** Start from the measurement, then judge what it cannot: read `.impeccable/review/diff/final/report.json` (and hero) first; every region scored `missing` or `contradicted` is a matrix row in that state unless the paired crop under `regions/` shows the score is wrong, and you say why; a region scored `match` still gets your eye for lettering character and material, which the numbers do not measure. Then, 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; 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. Three 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; medium is part of the promise. GROUND: the page field's value and temperature against the comp's, sampled from pixels on both sides when tooling allows rather than judged from memory, and read as the net on-screen result where a texture or tile paints over the base color; a ground warmer or cooler than the comp's is contradicted however faithfully the layout matches, and drift toward the rendition prior (warm cream on light grounds, blue-black slate on dark) is the direction to hunt. With no approved comp, 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 renders) as contradicted on its face; imitation material is the single most reliable mark of machine-made design. GROUND narrows rather than lapses: with no comp to sample, a color OWN-WORLD names is the target and the same warmer-or-cooler judgment applies; when OWN-WORLD names none, there is no GROUND authority, and the review says so in place of a verdict, because a target the reviewer invents turns the check into taste. A critique-reference comp on such a build is provocation, not spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is what the image dared that the build did not, and 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. A fix that requires producing an asset says so explicitly ("produce: <region> 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. 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, 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. 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, 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.
@@ -0,0 +1,101 @@
> **Additional context needed**: only the target element, when the request does not name one that resolves uniquely on the page.
Generate is the fast lane into live mode: the user names an element, a direction, and a count in one sentence, and within a minute they are cycling through variants in their browser. One command boots the helper, hands the element to the overlay in the page your harness already shows (it scrolls to it, selects it, and fires the same Go a click fires) and returns the generate event; one edit writes the variants; one call replies and waits for the user's choice, which the helper bakes into source itself. This file owns the lane's plumbing; from the event onward the design work is [live.md](live.md)'s, unchanged, so read it in full now if you have not this session.
**Web only.** Live mode's browser overlay has no native equivalent; on `ios` / `android` / `adaptive` projects, decline this command and offer `bolder` or `quieter` on the source instead.
The plumbing is where the lane saves time: one command starts the session around the page your harness already shows, one call replies and waits, and nothing here is a browser you have to babysit. The design work is not where it saves time. Setup runs as for any command (`impeccable context`, this reference, craft-floor.md before the edit), and the variants are planned, written, and accepted exactly the way a live session plans, writes, and accepts them.
Three prohibitions cover the known ways this command goes wrong:
- **Never run init or document, and never ask for PRODUCT.md or DESIGN.md.** When they exist, the start command prints them under `boot` and you use them. When they do not, it says so (`contextMissing`, `contextNote`) and you extract the identity from the event (Step 3). A missing file is never a reason to interview the user inside this command; offer `init` in one line after the session ends.
- **Never hand-write a variants wrapper or invent a session id.** Only the browser mints session ids (8 hex characters, at Go). A missing event is fixed by rerunning Step 2, never with a direct source edit.
- **Do not act on hook findings while live markers are in the file**, and do not restyle variants to appease them; the accept verifies the file once the variant is permanent.
## Step 1: Parse the request
Three parts, all from the user's sentence:
- **A number in the request**: that is the count. **No number**: 3. The protocol caps count at 8.
- **The direction wording** maps onto the live action vocabulary; never invent a new action value:
- **bold, bolder, stronger, punchier**: `bolder`
- **quiet, calmer, softer, toned down**: `quieter`
- **simpler, minimal, stripped**: `distill`
- **refined, tightened, polished**: `polish`
- **font and type words**: `typeset`
- **color words**: `colorize`
- **arrangement and spacing words**: `layout`
- **device and breakpoint words**: `adapt`
- **motion words**: `animate`
- **playful words**: `delight`
- **rule-breaking words**: `overdrive`
- **Wording that carries intent but no vocabulary word** ("make it feel like a bank", "warmer", "more premium"): `impeccable`, with the user's wording passed as the prompt.
- **An action fits AND extra intent rides along** ("bolder, but keep it monochrome"): that action, with the rest as the prompt.
- **The wording names no direction at all** ("better", "improve", "nicer", "different", "fresh", "new", "redesign", "fix", "some options", "ideas", "alternatives", or just "variants" with nothing else): STOP and call the ask_user_question tool to clarify. Ask one question, offering the vocabulary: *"Which direction should the variants take? bolder, quieter, simpler (distill), polished, typography (typeset), color (colorize), layout, motion (animate), playful (delight), or rule-breaking (overdrive)."* Map the answer with this list; an answer that is still open ("surprise me", "you pick") is `impeccable` with the user's original wording as the prompt, and Step 2 starts on that answer.
- **The element description** ("the pricing cards", "the hero heading"): Step 2 resolves it to a selector.
Done when you hold an action from the vocabulary (asked for, when the request named no direction), a count from 1 to 8, and the element description.
## Step 2: Reuse the page, then start
**Reuse** the dev server already running and the tab your harness already shows it in; a second server or a second browser window is the failure this step prevents.
1. **Find the dev server**, cheapest source first, and stop at the first hit: the user's message, a browser tab already on the app (Claude Code: an origin in `tabs_context`), a server your harness started (Claude Code: `preview_list`), a terminal that printed its URL. Its origin is your `--dev-url`. **No hit**: leave `--dev-url` off and run the start command with no wait; the boot probes for a running server and its verdict names the move. `browser_needed` carries the `devUrl` it found: open it as in 2, then rerun with `--dev-url <devUrl> --wait-for-browser 60000`. `no_dev_server` means nothing serves the app: start the dev script the way the verdict says (Claude Code: `preview_start`; Cursor: a background terminal; Codex: an exec you yield from), wait for its URL, then rerun with `--dev-url <url>`.
2. **Open the page that renders the element in your browser, then start.** The route the request names, else the one `--target` serves; `--dev-url` takes only the origin.
- **Cursor** (`browser_navigate`) and **Claude Code** (`navigate`, which opens the Browser pane when it is closed and takes the `tabId` from `tabs_context` when a tab is already on that origin): open the URL, then run the start command with `--dev-url <url> --wait-for-browser 60000`. The boot injects the overlay and the page reloads into it while the command waits. Your browser tool is the only opener on these harnesses; the engine ignores `--open` there.
- **No browser tool** (Codex, others): run the start command with `--open --wait-for-browser 120000`; it opens the system browser, and the longer wait covers the user finding the tab. **`browser_open_failed` back**: tell the user the `url` in one line and rerun with `--wait-for-browser 120000`.
```bash
.dsh/skills/impeccable/scripts/impeccable live-generate --target src/App.jsx --dev-url http://127.0.0.1:5173/ --selector ".pricing-grid" --action bolder --count 3 --boot --wait-for-browser 60000
```
Run it in the foreground in Cursor and Claude Code (it returns within the wait); on Codex, in an exec you yield from, the way Step 3 runs the poll.
- `--target`: the file that renders the element when the request or the project makes it obvious; skip it otherwise.
- `--dev-url`: the origin from 1; omit it and the boot probes.
- `--selector`: a unique class first, then a landmark tag plus class, an id last (every variant mounts a copy of the element, so an id repeats in the DOM). **The request names a repeated component in plural** ("the pricing cards"): target the container that holds the set, so one scoped stylesheet restyles every instance. One read of the source file that renders the element is allowed when the selector is not obvious; `--dry-run` resolves and reports without starting anything when it is not certain.
- `--boot`: runs the lane's boot (PRODUCT.md and DESIGN.md loaded again for the helper, missing files tolerated, dev URL found, bottom bar hidden for the helper's lifetime) and reuses a helper that is already running. Its result rides along as `boot`.
- Also available: `--prompt`, `--text` (keep only matches whose visible text contains a snippet), `--index` (1-based pick among matches).
Read the output in this order: `boot` (or `boot.contextMissing` with `boot.contextNote`: the page is the source of truth, per the note), then `event`, the generate event for `sessionId`, with the same `_instructions` a user's Go gets. Every verdict carries `_instructions`, and they win over your recollection of this file; the ones whose move is a decision of yours:
- **`ambiguous`**: the candidates are listed; target their common container, or rerun with `--text "<visible text>"` or `--index <n>`.
- **`dev_server_gone`**: the dev server stopped answering while the command waited for the page (on Cursor, a server another chat started dies with that chat). Start it the way the verdict says, then rerun with `--dev-url <url>`.
- **`no_match`**: the tab is on a route that does not render the element (navigate to the right route, rerun), or the selector is wrong (derive a better one from the source, or add `--text`).
- **`config_missing` / `config_invalid`** under `bootError`: follow [live-setup.md](live-setup.md) first, then rerun.
- **`event: null`** with `ok: true`: the event was slower than the wait; run `.dsh/skills/impeccable/scripts/impeccable live-poll` once to collect it, then continue.
Done when the output shows `ok: true`, a `sessionId`, and an `event`, reached with at most one server started and one tab opened by you.
## Step 3: Generate
The event is a standard `generate` event: the picked element's context, a preflighted scaffold, and `_instructions` naming the action's reference, the planning section, and the exact splice. Handle it exactly per live.md's **Handle generate**, which owns everything from the identity lock to the done reply: read the action's reference and craft-floor.md as it says, plan per section 4 (identity first, then mode, then three different primary axes, then the squint test), declare knobs per section 7, and deliver per section 6 (a complete replacement of the element per variant, the preview CSS plus every variant in one edit at the scaffold's splice). The lane changes nothing about what a variant may be: the moves a live session would make on this element (a promoted tier, a restructured set, a reordered card, a different surface) are open here too. Never screenshot the page; the overlay preview is the review channel until accept.
**Reply and wait in one call**, with the file you wrote:
```bash
.dsh/skills/impeccable/scripts/impeccable live-poll --reply EVENT_ID done --file src/App.jsx --then-poll
```
This replies done (the browser mounts the variants) and then blocks until the user's choice arrives, so run it the way your harness runs a long wait: **Claude Code** in the foreground with your tool's longest timeout (600000 ms), so you are paused until the choice arrives; **Codex** in a yielded foreground exec; **Cursor** in a background terminal with notify on `"type":"(accept|discard|variant_mount_failed|exit)"`. Never pass a short `--timeout=`. While it runs there is nothing else to do: never sleep and never poll its output on a timer; a harness that backgrounds it wakes you when it returns. `{"type":"timeout"}` means the user has not chosen yet: run `live-poll` again and keep waiting. If the edit fails after the browser flipped to GENERATING, `--reply EVENT_ID error "Short reason"` (without `--then-poll`) so the bar resets.
Then tell the user, in one line, where their variants are: *"Three [bolder] variants are live on [the pricing cards]: cycle with the floating bar's arrows, adjust the Tune knobs, and Accept the keeper."*
Outside the replace path, read the matching live.md section before acting: `scaffold.previewMode: "svelte-component"` (Svelte previews are edited as components, and their accept is mechanical), `mode: "insert"`, `variant_mount_failed`, `steer`, `manual_edit_apply`, and any `fallback: "agent-driven"` wrap error.
## Step 4: Accept and close
The call from Step 3 returns the user's choice. **`discard`**: nothing to do. **`accept`**: `_acceptResult.carbonize: true` is the normal case, and the cleanup is live.md's **Required after accept**, unchanged: move the accepted variant's rules into the stylesheet that already owns the element with real selectors, bake the chosen knob values in, unwrap the element and drop every `data-impeccable-*` attribute, delete the inline `<style>` block and both `impeccable-carbonize` markers, then `.dsh/skills/impeccable/scripts/impeccable live-complete --id SESSION_ID` and confirm `phase: "completed"`. (`baked: true` appears only when the accept was run with `--bake`; then the helper already made the variant permanent and no `live-complete` is owed.)
Close without being asked, the moment the choice is handled:
```bash
.dsh/skills/impeccable/scripts/impeccable live-server stop
```
Stopping removes the injected script and reloads the page once: the user sees the accepted design with no overlay chrome, still served by their dev server. **Never kill or restart the dev server**, including one you started in Step 2.
- **The user asks for more variants before you closed**: skip the close, run Step 2 again for the next element (the helper is reused), and close after the last choice.
- **Interrupted or unsure of the state**: `.dsh/skills/impeccable/scripts/impeccable live-status`, then `live-resume`; the journal under `.impeccable/live/sessions/` is canonical.
Done when the helper is stopped and the dev site still answers with the accepted design.
@@ -205,6 +205,11 @@ t('items', { count }) // Handles complex plural rules
- Optimistic updates with rollback - Optimistic updates with rollback
- Conflict resolution - Conflict resolution
**Interrupted gestures** (custom sliders, drag surfaces, scrollable control strips):
- A second finger or pointer lands mid-drag: the first drag keeps its pointer or ends cleanly, never jumps to the new one
- The browser cancels the gesture to scroll (`pointercancel`), capture is lost (`lostpointercapture`), the pointer is released outside the control, or the window loses focus (`blur`) mid-drag: clear the dragging state and release capture
- After each of these, the next tap or drag works without a reload
**Permission states**: **Permission states**:
- No permission to view - No permission to view
- No permission to edit - No permission to edit
@@ -304,6 +309,7 @@ const throttledScroll = throttle(handleScroll, 100);
- Unit tests for edge cases - Unit tests for edge cases
- Integration tests for error scenarios - Integration tests for error scenarios
- E2E tests for critical paths - E2E tests for critical paths
- A behavioral regression for each confirmed gesture fix, when the project's test runner can drive input
- Visual regression tests - Visual regression tests
- Accessibility tests (axe, WAVE) - Accessibility tests (axe, WAVE)
@@ -330,7 +336,10 @@ Test thoroughly with edge cases:
- **Network issues**: Disable internet, throttle connection - **Network issues**: Disable internet, throttle connection
- **Large datasets**: Test with 1000+ items - **Large datasets**: Test with 1000+ items
- **Concurrent actions**: Click submit 10 times rapidly - **Concurrent actions**: Click submit 10 times rapidly
- **Interrupted gestures**: Add a second finger mid-drag, scroll across the control, release outside it, switch windows mid-drag; then drag again
- **Errors**: Force API errors, test all error states - **Errors**: Force API errors, test all error states
- **Empty**: Remove all data, test empty states - **Empty**: Remove all data, test empty states
For gestures, say what produced the evidence (emulated viewport, synthesized touch, which engine, physical device) and name what stayed untested.
When edge cases are covered, hand off to `/impeccable polish` for the final pass. When edge cases are covered, hand off to `/impeccable polish` for the final pass.
+3 -6
View File
@@ -96,7 +96,7 @@ Build the assigned direction, not a safer interpretation of it. The form supplie
When an approved comp exists, it is a spatial contract, not a mood board: only the user can downgrade its authority, in explicit words. Models systematically believe their HTML, CSS, and SVG recreation of an image succeeded when it did not, so the build runs as a state machine on disk whose gates measure the screen against the comp instead of asking you to remember it. Start it once, and let it tell you what is next: When an approved comp exists, it is a spatial contract, not a mood board: only the user can downgrade its authority, in explicit words. Models systematically believe their HTML, CSS, and SVG recreation of an image succeeded when it did not, so the build runs as a state machine on disk whose gates measure the screen against the comp instead of asking you to remember it. Start it once, and let it tell you what is next:
`.dsh/skills/impeccable/scripts/impeccable build-phase start --direction <seed key> --kind <assigned|pick|challenger|canon> --artifact <entry file>` right after the direction choice (this is also the choice ping; the roll's output names the exact command), or `start --comp <approved comp> --artifact <entry file>` when a surface round already locked one. `.dsh/skills/impeccable/scripts/impeccable build-phase start --direction <seed key> --kind <assigned|pick|challenger|canon>` right after the direction choice (this is also the choice ping; the roll's output names the exact command), or `start --comp <approved comp>` when a surface round already locked one.
Then, in order, each closed by `.dsh/skills/impeccable/scripts/impeccable build-phase advance` (every verb below runs as `.dsh/skills/impeccable/scripts/impeccable <verb>`; exit 2 means the gate failed and printed why; fix that and advance again; write nothing for a later phase while an earlier gate is open): Then, in order, each closed by `.dsh/skills/impeccable/scripts/impeccable build-phase advance` (every verb below runs as `.dsh/skills/impeccable/scripts/impeccable <verb>`; exit 2 means the gate failed and printed why; fix that and advance again; write nothing for a later phase while an earlier gate is open):
@@ -104,9 +104,8 @@ Then, in order, each closed by `.dsh/skills/impeccable/scripts/impeccable build-
The comp-led path is a frontier-tier job: it asks the builder to hold a measured layout, place plates at their boxes, and act on numeric readings across a dozen attempts. Smaller or faster models produce a recognisable page and stall under the hero gate; if the model in hand is one of those, say so before the direction round and take the code-led path, or expect the run to end at the hero with its readings unmet. The comp-led path is a frontier-tier job: it asks the builder to hold a measured layout, place plates at their boxes, and act on numeric readings across a dozen attempts. Smaller or faster models produce a recognisable page and stall under the hero gate; if the model in hand is one of those, say so before the direction round and take the code-led path, or expect the run to end at the hero with its readings unmet.
1. **spec.** Measure the comp: `impeccable comp-spec --comp <comp> --grid` writes a coordinate grid over the comp; open it, name every salient region by grid span in a regions file (text and control regions snap to the largest ink mass inside their span, so a headline named B1:E4 measures as the headline and not the column beside it; `snap: false` keeps the span, and an explicit `box` is taken as drawn) (kind `plate` / `image` / `texture` for anything painted: every illustration, photograph, figure, product object, and material texture; `text` / `control` / `chrome` for what code draws; every region carries a `note` saying what the comp shows there, which the plate prompt and the gate messages read), and run `impeccable comp-spec --comp <comp> --regions <file>`. The spec carries each region's box, sampled palette, and medium; `impeccable comp-spec --print` is the build's reference from here on. Type is measured, not guessed: `impeccable font-match --measure <text region>` reads the comp's cap height, width class, and weight off the pixels, and `impeccable font-match --rank <region> --text "..."` takes its candidates from a fingerprint index of the Google Fonts catalog (the nearest faces to the crop's shape) plus any names you pass with `--candidates`, renders them at that cap height with the region's words, and ranks them by fingerprint distance (its `USE` line is the CSS; its proof sheet shows the comp over the top three); with no browser resolvable it records the catalog's nearest face and says the size is estimated, which is still the choice to build on. Do not install a browser to rank, and never write a `chosen` face into the spec by hand: the gate accepts only what font-match wrote. The spec gate refuses to close until the lead text region is measured and ranked. A region note that describes painted material (a diagram, drawing, photograph, texture) under a code kind is refused at the spec: reclassify it as a plate, or reword the note if code really draws it. The script refuses a regions file that leaves comp ink unnamed (callouts, a parts table, a notes block): what is never named can never be missing, so everything the comp shows gets a region. It also refuses a `text` / `control` / `chrome` region larger than a quarter of the comp: that is a column, not an element, and a column scored as one region hides the plates, tables, and notes inside it. Name each element inside it (`container: true` only when it truly is one undivided element). Anything drawn is a plate: an inline SVG past an icon's budget (a diagram, notation, leader lines with arrows, a "quick approximation" of the artwork) is refused at the hero; icon-sized SVG (under 64px, a few paths) is fine, and a chart the page draws from data at runtime is a chart, not an illustration. Callout lines and arrows that annotate a drawing belong to that drawing's plate, with only their labels set as text. A crop of the comp is never a plate (the plates gate refuses a file that is a resample of the comp region: the comp's grain, its neighbours' edges, and its resolution would ship as the artwork); the crop is the reference the plate is generated from. A plate region's box has to hold its whole artwork with a margin: the spec measures the artwork's contact with the box edges and refuses a box that cuts through it (`bleed: true` only when the page really crops it there), because a plate placed with `object-fit: cover` on such a box shows the artwork minus the side the box lost. Anything not in the spec does not exist on the page: no borders, rules, containers, or chrome the comp does not show. Only three concessions exist: fonts (the closest obtainable face), icon glyphs (close enough, exact if the user chose an icon library; this covers the pictogram only, never a control's chrome, so a chevron, an arrow, a dropdown's border and fill, a button's shape are the comp's), and genuine defects in the comp such as spelling errors. 1. **spec.** Measure the comp: `impeccable comp-spec --comp <comp> --grid` writes a coordinate grid over the comp; open it, name every salient region by grid span in a regions file (text and control regions snap to the largest ink mass inside their span, so a headline named B1:E4 measures as the headline and not the column beside it; `snap: false` keeps the span, and an explicit `box` is taken as drawn) (kind `plate` / `image` / `texture` for anything painted: every illustration, photograph, figure, product object, and material texture; `text` / `control` / `chrome` for what code draws; every region carries a `note` saying what the comp shows there, which the plate prompt and the gate messages read), and run `impeccable comp-spec --comp <comp> --regions <file>`. The spec carries each region's box, sampled palette, and medium; `impeccable comp-spec --print` is the build's reference from here on. Type is measured, not guessed: `impeccable font-match --measure <text region>` reads the comp's cap height, width class, and weight off the pixels, and `impeccable font-match --rank <region> --text "..."` takes its candidates from a fingerprint index of the Google Fonts catalog (the nearest faces to the crop's shape) plus any names you pass with `--candidates`, renders them at that cap height with the region's words, and ranks them by fingerprint distance (its `USE` line is the CSS; its proof sheet shows the comp over the top three); with no browser resolvable it records the catalog's nearest face and says the size is estimated, which is still the choice to build on. Do not install a browser to rank, and never write a `chosen` face into the spec by hand: the gate accepts only what font-match wrote. The spec gate refuses to close until the lead text region is measured and ranked. A region note that describes painted material (a diagram, drawing, photograph, texture) under a code kind is refused at the spec: reclassify it as a plate, or reword the note if code really draws it. The script refuses a regions file that leaves comp ink unnamed (callouts, a parts table, a notes block): what is never named can never be missing, so everything the comp shows gets a region. It also refuses a `text` / `control` / `chrome` region larger than a quarter of the comp: that is a column, not an element, and a column scored as one region hides the plates, tables, and notes inside it. Name each element inside it (`container: true` only when it truly is one undivided element). Anything drawn is a plate: an inline SVG past an icon's budget (a diagram, notation, leader lines with arrows, a "quick approximation" of the artwork) is refused at the hero; icon-sized SVG (under 64px, a few paths) is fine, and a chart the page draws from data at runtime is a chart, not an illustration. Callout lines and arrows that annotate a drawing belong to that drawing's plate, with only their labels set as text. A crop of the comp is never a plate (the plates gate refuses a file that is a resample of the comp region: the comp's grain, its neighbours' edges, and its resolution would ship as the artwork); the crop is the reference the plate is generated from. A plate region's box has to hold its whole artwork with a margin: the spec measures the artwork's contact with the box edges and refuses a box that cuts through it (`bleed: true` only when the page really crops it there), because a plate placed with `object-fit: cover` on such a box shows the artwork minus the side the box lost. Anything not in the spec does not exist on the page: no borders, rules, containers, or chrome the comp does not show. Only three concessions exist: fonts (the closest obtainable face), icon glyphs (close enough, exact if the user chose an icon library; this covers the pictogram only, never a control's chrome, so a chevron, an arrow, a dropdown's border and fill, a button's shape are the comp's), and genuine defects in the comp such as spelling errors.
2. **plates.** Every raster region ships as a plate: an illustration, photo, or figure regenerated at asset resolution from its comp crop, UI text removed, at its `plate` path (isolated ink, figures, or objects use native transparent PNG so they sit on the page's own ground; photos and textures stay opaque); a texture (paper, cloth, grain) is a clean patch of the comp region mirror-tiled to size, generated only when no clean patch exists. `impeccable comp-spec --crop <id>` writes the reference; save `impeccable comp-spec --plate-prompt <id> --background transparent` to a prompt file for a cutout, or use `--background opaque` otherwise. Prefer the harness-native image tool with that crop and prompt, then `impeccable embed-prompt <plate> --prompt-file <prompt.txt>`. The API fallback is `impeccable generate-image --ref <crop.png> --prompt-file <prompt.txt> --out <plate.png> --size <WxH> --quality high --background transparent` (use `--background opaque` for full-frame assets); create the output directory first. Verify actual alpha, white foregrounds, fine edges, and clear holes on light and dark grounds; do not chroma-key native output. After the initial assets exist, prepare the isolated code component previews and complete [component-review.md](component-review.md) before further gate-driven repair: the user reviews the whole component kit, including code, before page assembly. This checkpoint keeps the existing gates; it does not require passing them first. After the full page and responsive checks, use the assembled-hero checkpoint before the final response. The plates gate scores the assets against the comp; also inspect placement and scale visually. With parallel subagents, spawn the shipped asset producer (`impeccable-asset-producer`; `impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent") with the spec path and let it produce them all; without subagents, produce them here. A crop of the comp is a reference, never a shipping pixel. The gate checks every plate exists, is at least 1.5x the region's size, and reads as the region. Page code waits for this gate: a page written before its plates exist is a page that draws its material in CSS. A single-file deliverable changes nothing here: the plate is produced the same way and inlined as a data URI. `--force` exists for one case only, the user downgrading the comp's authority in words you quote in `--reason`; the script refuses every other reason. 2. **plates.** Every raster region ships as a plate: an illustration, photo, or figure regenerated at asset resolution from its comp crop, UI text removed, at its `plate` path (isolated ink, figures, or objects use native transparent PNG so they sit on the page's own ground; photos and textures stay opaque); a texture (paper, cloth, grain) is a clean patch of the comp region mirror-tiled to size, generated only when no clean patch exists. `impeccable comp-spec --crop <id>` writes the reference; save `impeccable comp-spec --plate-prompt <id> --background transparent` to a prompt file for a cutout, or use `--background opaque` otherwise. Prefer the harness-native image tool with that crop and prompt, then `impeccable embed-prompt <plate> --prompt-file <prompt.txt>`. The API fallback is `impeccable generate-image --ref <crop.png> --prompt-file <prompt.txt> --out <plate.png> --size <WxH> --quality high --background transparent` (use `--background opaque` for full-frame assets); create the output directory first. Verify actual alpha, white foregrounds, fine edges, and clear holes on light and dark grounds; do not chroma-key native output. The plates gate scores the assets against the comp; also inspect placement and scale visually. With parallel subagents, spawn the shipped asset producer (`impeccable-asset-producer`; `impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent") with the spec path and let it produce them all; without subagents, produce them here. A crop of the comp is a reference, never a shipping pixel. The gate checks every plate exists, is at least 1.5x the region's size, and reads as the region. Page code waits for this gate: a page written before its plates exist is a page that draws its material in CSS. A single-file deliverable changes nothing here: the plate is produced the same way and inlined as a data URI. `--force` exists for one case only, the user downgrading the comp's authority in words you quote in `--reason`; the script refuses every other reason.
3. **hero.** `impeccable build-phase scaffold` first: it writes the measured layout as CSS custom properties (`.impeccable/build/scaffold/layout.css`: `--r-<id>-x/y/w/h` in % of the comp, plus cap height, font-size, family, and weight where measured) and a reference page (`hero-reference.html`) with every region at its box and every plate placed. Bind the numbers to your own semantic structure, an element per region; the reference is a check on positions, never the page, and overlapping boxes are overlapping boxes. Then build only the first viewport, at the comp's own dimensions, the comp's words copied verbatim (the user approved that comp with those words; rewording is a stated decision after the hero passes, never a silent one inside it), every text region sized from its measured cap height and set in its ranked face, plates first: place every plate at its spec box (`object-fit: cover`, an `<img>`, a background image, or an inlined data URI named for it) before any text or control, capture into `.impeccable/review/hero-repro.png`, run `impeccable build-phase record hero` once so you see the plate regions read as match before any text exists, then lay the semantic layer over the plates from the spec's palette and boxes and advance. The gate first refuses while any plate is unreferenced by the source, then runs `impeccable comp-diff`, writes `.impeccable/review/diff/hero/` (side-by-side, heatmap, one paired crop per region, `report.json`), and passes at 72% overall with no hard veto outstanding (a missing region, a contradicted plate or text block, an SVG illustration, a clipped plate, invented ink block at any score); above the bar, the numeric readings become advisories printed with the pass, and the polish pass before responsive is where they get fixed: the gate also reads each text region's cap height, line count, weight, ink colour, and position against the comp, each chrome strip's height off its rule, and the frame for ink where the comp is calm (a kicker, an extra nav item, a divider), and says each miss as a number ("cap height 78px in the build, 103px in the comp"); those numbers are the edit. When it fails, open the region crops it lists, in order, before editing: a region scored `missing` needs its material, `contradicted` needs its structure re-derived from the spec box, `drift` is where size and spacing edits belong; the gate refuses a third attempt that only nudges values on the same region. This is where the run's ambition is won or lost, and a retry here costs minutes where a rebuild verdict at the finish costs the run.
3. **hero.** `impeccable build-phase scaffold` first: it writes the measured layout as CSS custom properties (`.impeccable/build/scaffold/layout.css`: `--r-<id>-x/y/w/h` in % of the comp, plus cap height, font-size, family, and weight where measured) and a reference page (`hero-reference.html`) with every region at its box and every plate placed. Bind the numbers to your own semantic structure, an element per region; the reference is a check on positions, never the page, and overlapping boxes are overlapping boxes. Then build only the first viewport, at the comp's own dimensions, the comp's words copied verbatim (the user approved that comp with those words; rewording is a stated decision after the hero passes, never a silent one inside it), every text region sized from its measured cap height and set in its ranked face, plates first: place every plate at its spec box (`object-fit: cover`, an `<img>`, a background image, or an inlined data URI named for it) before any text or control, capture into `.impeccable/review/hero-repro.png`, run `impeccable build-phase record hero` once so you see the plate regions read as match before any text exists, then lay the semantic layer over the plates from the spec's palette and boxes and advance. The gate first refuses while any plate is unreferenced by the source, then runs `impeccable comp-diff`, writes `.impeccable/review/diff/hero/` (side-by-side, heatmap, one paired crop per region, `report.json`; `raw-report.json` preserves the uninterpreted measurements). The report and crop labels use the gate's verdicts; `gate.reasons` lists the remaining blockers even when a region is called drift. An accepted plate is revalidated if its file, measured region, or comp changes. The gate passes at 72% overall with no hard veto outstanding (a missing region, a contradicted plate or text block, an SVG illustration, a clipped plate, invented ink block at any score); above the bar, the numeric readings become advisories printed with the pass, and the polish pass before responsive is where they get fixed: the gate also reads each text region's cap height, line count, weight, ink colour, and position against the comp, each chrome strip's height off its rule, and the frame for ink where the comp is calm (a kicker, an extra nav item, a divider), and says each miss as a number ("cap height 78px in the build, 103px in the comp"); those numbers are the edit. When it fails, open the region crops it lists, in order, before editing: a region scored `missing` needs its material, `contradicted` needs its structure re-derived from the spec box, `drift` is where size and spacing edits belong; repeated attempts do not clear unresolved blockers. This is where the run's ambition is won or lost, and a retry here costs minutes where a rebuild verdict at the finish costs the run.
4. **sections.** Build the rest of the surface inside the spec's system: the same corner language, line weights, and palette, and nothing the comp never shows. Where the comp does not cover a region, it inherits the recorded system. 4. **sections.** Build the rest of the surface inside the spec's system: the same corner language, line weights, and palette, and nothing the comp never shows. Where the comp does not cover a region, it inherits the recorded system.
5. **motion.** The signature interaction, reveals, and motion, orchestrated once rather than scattered. 5. **motion.** The signature interaction, reveals, and motion, orchestrated once rather than scattered.
6. **responsive.** The other viewports, and the first viewport at common desktop widths (1280 to 1600), not only at the comp's exact size: fluid columns, no fixed-pixel grid that wraps a hundred pixels narrower. Capture `desktop.png` (1440 wide, full page) and `mobile.png` (390 wide) into `.impeccable/review/`; the gate diffs the desktop capture against the comp and refuses a first viewport that only held at the comp's width. A comp'd surface that is mobile-first was comped portrait; the plates were produced for that frame. 6. **responsive.** The other viewports, and the first viewport at common desktop widths (1280 to 1600), not only at the comp's exact size: fluid columns, no fixed-pixel grid that wraps a hundred pixels narrower. Capture `desktop.png` (1440 wide, full page) and `mobile.png` (390 wide) into `.impeccable/review/`; the gate diffs the desktop capture against the comp and refuses a first viewport that only held at the comp's width. A comp'd surface that is mobile-first was comped portrait; the plates were produced for that frame.
@@ -144,5 +143,3 @@ A rebuild and a fix round share one asset rule: a raster either round creates or
Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice.
After the last correction, spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, artifact path, direction contract, PRODUCT.md, [document.md](document.md), and write boundary. Without subagents, load [degraded/documenter.md](degraded/documenter.md) and [document.md](document.md) before writing. Verify the outcome: new worlds and approved system changes require token-bearing DESIGN.md **and** `.impeccable/design.json`, not prose alone. Ordinary extensions compare the finished build against the incumbent system, preserve its files, and report the evidence checked; report pre-existing drift without repairing it unasked. Recheck after later edits. Finish only when review and documentation are complete. After the last correction, spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, artifact path, direction contract, PRODUCT.md, [document.md](document.md), and write boundary. Without subagents, load [degraded/documenter.md](degraded/documenter.md) and [document.md](document.md) before writing. Verify the outcome: new worlds and approved system changes require token-bearing DESIGN.md **and** `.impeccable/design.json`, not prose alone. Ordinary extensions compare the finished build against the incumbent system, preserve its files, and report the evidence checked; report pre-existing drift without repairing it unasked. Recheck after later edits. Finish only when review and documentation are complete.
On a comp-led build, record the final review disposition with `.dsh/skills/impeccable/scripts/impeccable build-phase finish --disposition <ship|fix|rebuild|recapture>` before the final response. A refused `ship` is an unfinished build; report the outstanding phase with the verdict.
+1 -1
View File
@@ -16,7 +16,7 @@ Reason over the signals; there is no score to obey:
- `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique <surface>` is a strong default. - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique <surface>` is a strong default.
- `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog and closes it when stale or cleared). - `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog and closes it when stale or cleared).
- `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them.
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. **`live` and the bundled `impeccable detect` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with either; the browser overlay and the HTML rule engine don't apply to native app code. - `devServer.running` true → `live` is available for in-browser iteration, and `generate` for one-shot variant runs on a named element; if false, don't lead with either. **`live`, `generate`, and the bundled `impeccable detect` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with any of them; the browser overlay and the HTML rule engine don't apply to native app code.
- Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`. - Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`.
**If `scan.targets` is non-empty and `setup.platform` is not `ios`/`android`/`adaptive`, run `.dsh/skills/impeccable/scripts/impeccable detect --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx; it reads HTML/CSS, so skip it for native projects). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it. **If `scan.targets` is non-empty and `setup.platform` is not `ios`/`android`/`adaptive`, run `.dsh/skills/impeccable/scripts/impeccable detect --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx; it reads HTML/CSS, so skip it for native projects). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
+1 -1
View File
@@ -1 +1 @@
0.1.6 0.1.5
@@ -19,6 +19,10 @@
"description": "Interactive live variant mode. Select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via HMR. Requires a running dev server. Use when you want to visually experiment with design alternatives in real time.", "description": "Interactive live variant mode. Select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via HMR. Requires a running dev server. Use when you want to visually experiment with design alternatives in real time.",
"argumentHint": "" "argumentHint": ""
}, },
"generate": {
"description": "Agent-driven live variant generation. Boots live mode, finds the named element on the open page, scrolls the browser to it, and delivers N variants in the requested direction for the user to cycle and accept. Use for requests that name an element and a direction, like 'generate 3 bold variants of the pricing cards', skipping manual element picking.",
"argumentHint": "[count] [direction] variants of [element]"
},
"adapt": { "adapt": {
"description": "Adapt designs to work across different screen sizes, devices, contexts, or platforms. Implements breakpoints, fluid layouts, and touch targets. Use when the user mentions responsive design, mobile layouts, breakpoints, viewport adaptation, or cross-device compatibility.", "description": "Adapt designs to work across different screen sizes, devices, contexts, or platforms. Implements breakpoints, fluid layouts, and touch targets. Use when the user mentions responsive design, mobile layouts, breakpoints, viewport adaptation, or cross-device compatibility.",
"argumentHint": "[target] [context (mobile, tablet, print...)]" "argumentHint": "[target] [context (mobile, tablet, print...)]"
+524 -4
View File
@@ -165,6 +165,14 @@
} }
let parameterGenerationState = 'idle'; let parameterGenerationState = 'idle';
let parameterReadyAnnouncedSession = null; let parameterReadyAnnouncedSession = null;
// 'agent' when the generate verb fired this session's Go (the generate
// lane declares no knobs, so its bar never shows a pending Tune chip);
// null for every Go a user presses.
let sessionOrigin = null;
// The generate lane picks for the agent and never edits copy in the
// browser, so its selection carries no edit-copy badge (set on the
// agent-target pick, cleared with the session; a user's pick never sets it).
let editBadgeSuppressed = false;
let svelteComponentSession = null; let svelteComponentSession = null;
let svelteRuntimePromise = null; let svelteRuntimePromise = null;
let pendingSvelteComponentRetryObserver = null; let pendingSvelteComponentRetryObserver = null;
@@ -983,9 +991,20 @@
} }
} catch { /* cross-origin */ } } catch { /* cross-origin */ }
} }
// The selector a mechanical bake would anchor lasting rules on, and how
// many elements it matches right now: the bake refuses anything but one,
// since its rules would restyle every match, not just this element.
const cssIdent = (s) => /^[A-Za-z_-][\w-]*$/.test(s);
const anchorClasses = [...el.classList].filter(cssIdent);
const anchor = el.id && cssIdent(el.id)
? '#' + el.id
: (anchorClasses.length ? el.tagName.toLowerCase() + '.' + anchorClasses.join('.') : null);
let anchorMatches = null;
if (anchor) { try { anchorMatches = document.querySelectorAll(anchor).length; } catch { anchorMatches = null; } }
return { return {
tagName: el.tagName.toLowerCase(), id: el.id || null, tagName: el.tagName.toLowerCase(), id: el.id || null,
classes: [...el.classList], classes: [...el.classList],
anchor, anchorMatches,
textContent: (el.textContent || '').slice(0, 500), textContent: (el.textContent || '').slice(0, 500),
outerHTML: sanitizedContextOuterHTML(el, 10000), outerHTML: sanitizedContextOuterHTML(el, 10000),
computedStyles: { computedStyles: {
@@ -2037,6 +2056,7 @@
function setLiveState(next) { function setLiveState(next) {
state = next; state = next;
window.__IMPECCABLE_LIVE_STATE__ = next; window.__IMPECCABLE_LIVE_STATE__ = next;
retryDeclinedAgentTargets();
syncPageInteractionCursor(); syncPageInteractionCursor();
// Whether a queued steer is still behind a generation is a function of this // Whether a queued steer is still behind a generation is a function of this
// state, so the hint has to move with it, not only with the 5s poll. // state, so the hint has to move with it, not only with the 5s poll.
@@ -4014,6 +4034,7 @@
function hidePendingApplyDock() { function hidePendingApplyDock() {
pendingApplyInFlight = false; pendingApplyInFlight = false;
retryDeclinedAgentTargets();
clearStoredManualApplyState(); clearStoredManualApplyState();
if (pendingIntroAnimation) { pendingIntroAnimation.cancel(); pendingIntroAnimation = null; } if (pendingIntroAnimation) { pendingIntroAnimation.cancel(); pendingIntroAnimation = null; }
if (pendingDockEl) pendingDockEl.style.display = 'none'; if (pendingDockEl) pendingDockEl.style.display = 'none';
@@ -4047,6 +4068,7 @@
function setPendingApplyLoading(loading, count) { function setPendingApplyLoading(loading, count) {
if (!pendingPillEl || !pendingPillLabelEl || !pendingPillCountEl || !pendingTrashBtn) return; if (!pendingPillEl || !pendingPillLabelEl || !pendingPillCountEl || !pendingTrashBtn) return;
pendingApplyInFlight = loading === true; pendingApplyInFlight = loading === true;
if (!pendingApplyInFlight) retryDeclinedAgentTargets();
const currentCount = count || parseInt(pendingPillEl.dataset.count || '0', 10) || 0; const currentCount = count || parseInt(pendingPillEl.dataset.count || '0', 10) || 0;
if (pendingApplyInFlight) storeManualApplyState(currentCount); if (pendingApplyInFlight) storeManualApplyState(currentCount);
else clearStoredManualApplyState(); else clearStoredManualApplyState();
@@ -4688,6 +4710,7 @@
} }
function renderEditBadge(mode) { function renderEditBadge(mode) {
if (editBadgeSuppressed || sessionOrigin === 'agent') mode = 'hidden';
if (mode === 'hidden' || !editBadgeEl) { if (mode === 'hidden' || !editBadgeEl) {
hideConfigureBarTooltip(); hideConfigureBarTooltip();
if (editBadgeEl) editBadgeEl.style.display = 'none'; if (editBadgeEl) editBadgeEl.style.display = 'none';
@@ -6181,6 +6204,8 @@
resetSessionFileMeta(); resetSessionFileMeta();
currentSessionId = null; currentSessionId = null;
parameterGenerationState = 'idle'; parameterGenerationState = 'idle';
sessionOrigin = null;
editBadgeSuppressed = false;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
expectedVariants = 0; expectedVariants = 0;
arrivedVariants = 0; arrivedVariants = 0;
@@ -7112,6 +7137,398 @@
} }
// //
// ------------------------------------------------------------------
// Agent-initiated targeting (the `generate` command). The agent names an
// element by CSS selector over POST /agent-target; the server pushes an
// `agent_target` SSE message here. The overlay resolves the selector,
// scrolls the element into view, enters the same picked state a user
// click produces, and fires the normal Go pipeline, so everything
// downstream (generate event, variants, cycling, accept) is unchanged.
// The verdict goes back through POST /agent-target-result, which resolves
// the agent's held-open CLI call.
function postAgentTargetResult(targetId, result) {
fetch('http://localhost:' + PORT + '/agent-target-result?token=' + TOKEN, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: TOKEN, targetId, clientId: AGENT_TARGET_CLIENT_ID, ...result }),
}).catch(() => { /* server gone; nothing to report to */ });
}
function describeAgentTargetCandidate(el) {
return {
tag: el.tagName.toLowerCase(),
id: el.id || null,
classes: [...el.classList].filter((c) => !c.startsWith('impeccable-')),
text: (el.textContent || '').trim().slice(0, 80),
};
}
function resolveAgentTargetElement(msg) {
let matched;
try {
matched = [...document.querySelectorAll(msg.selector)];
} catch {
return { error: { ok: false, error: 'invalid_selector', selector: msg.selector } };
}
let candidates = matched.filter((el) => pickable(el));
if (msg.text) {
const needle = String(msg.text).toLowerCase();
candidates = candidates.filter((el) => (el.textContent || '').toLowerCase().includes(needle));
}
if (candidates.length === 0) {
return {
error: {
ok: false,
error: 'no_match',
selector: msg.selector,
matchCount: 0,
// How many nodes the raw selector hit before the pickable/text
// filters: distinguishes a wrong selector from an unpickable match.
rawMatchCount: matched.length,
},
};
}
if (Number.isInteger(msg.index)) {
const el = candidates[msg.index - 1];
if (!el) {
return { error: { ok: false, error: 'index_out_of_range', selector: msg.selector, matchCount: candidates.length } };
}
return { el, matchCount: candidates.length };
}
if (candidates.length > 1) {
return {
error: {
ok: false,
error: 'ambiguous',
selector: msg.selector,
matchCount: candidates.length,
candidates: candidates.slice(0, 8).map(describeAgentTargetCandidate),
},
};
}
return { el: candidates[0], matchCount: 1 };
}
function scrollAgentTargetIntoView(el, done) {
const rect = el.getBoundingClientRect();
if (rect.top >= 0 && rect.bottom <= window.innerHeight) { done(); return; }
let settled = false;
let fallback = null;
const finish = () => {
if (settled) return;
settled = true;
removeEventListener('scrollend', finish, true);
if (fallback) clearTimeout(fallback);
done();
};
// scrollend where supported; a timer covers engines without it and the
// no-movement case (element already at its final resting position).
addEventListener('scrollend', finish, true);
fallback = setTimeout(finish, 1200);
el.scrollIntoView({ block: 'center', behavior: 'smooth' });
}
// One id per page load: the server keys claims and roll-call reports on
// it, and only the tab that holds the lease can renew it.
const AGENT_TARGET_CLIENT_ID = id8();
// The agent target an agent-initiated Go is serving: set by
// actOnAgentTarget around its handleGo call, read once by handleGo.
let agentTargetForGo = null;
// The helper's word on its global bar. The generate lane asks the helper
// to keep it out of the way (`impeccable live --no-live-bar`, or an agent
// target carrying hideLiveBar), and the helper tells every connected tab
// at once (`live_bar`) and every later connection on `connected`, so the
// bar stays hidden in every tab, through reloads, the accept, and the
// bake, until the helper stops and takes the overlay with it. The variant
// controls still show.
let liveBarHiddenByHelper = false;
function applyLiveBarPreference(hidden) {
liveBarHiddenByHelper = hidden === true;
setLiveBarHidden(liveBarHiddenByHelper);
}
// A plain live session must never notice this code: hiding remembers the
// bar's own display value and restoring puts exactly that back, and a
// restore on a bar that is not hidden is a no-op, so the `connected`
// frame every session receives changes nothing unless the lane asked.
function setLiveBarHidden(hidden) {
if (!globalBarEl) return;
if (hidden) {
if (globalBarEl.style.display !== 'none') {
globalBarEl.dataset.liveBarDisplay = globalBarEl.style.display || 'flex';
globalBarEl.style.display = 'none';
}
return;
}
if (globalBarEl.style.display === 'none') {
globalBarEl.style.display = globalBarEl.dataset.liveBarDisplay || 'flex';
}
}
function claimAgentTarget(targetId, report) {
return fetch('http://localhost:' + PORT + '/agent-target-claim?token=' + TOKEN, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: TOKEN, targetId, clientId: AGENT_TARGET_CLIENT_ID, ...report }),
}).then((res) => res.json())
.then((j) => ({ granted: !!j && j.granted === true, pending: !!j && j.pending === true }))
.catch(() => ({ granted: false, pending: false }));
}
// `exceptTargetId` is the target this call is about: a tab acting on it
// is not busy for itself, but it is busy for every other target, or two
// held requests could both be claimed here and the second Go would
// overwrite the session the first one minted.
function agentTargetBusyReason(exceptTargetId) {
if (pendingApplyInFlight) return 'manual_apply_in_flight';
if (state !== 'IDLE' && state !== 'PICKING' && state !== 'CONFIGURING') return 'session_active';
for (const [targetId, status] of agentTargetsSeen) {
if (status === 'acting' && targetId !== exceptTargetId) return 'agent_target_in_flight';
}
return null;
}
// Targets this tab declined as busy. A busy report is only this tab's word
// at that moment: the moment it is free again (setLiveState), it claims
// each of these as eligible, and the server drops the stale report, so a
// busy verdict is never built on a tab that has since gone idle. The
// server denies claims for resolved targets, so retries are harmless.
const busyDeclinedTargets = new Map();
function declineAgentTargetBusy(msg, busy) {
busyDeclinedTargets.set(msg.targetId, msg);
noteAgentTarget(msg.targetId, 'declined');
claimAgentTarget(msg.targetId, { eligible: false, state, reason: busy });
}
// A torn-down overlay, or one whose helper connection is gone, cannot
// serve a target and must not even claim one: it would hold the lease for
// a request it will never act on.
function agentTargetOverlayGone() {
return !evtSource;
}
// A denied claimant retries at this cadence, a little over the lease, so
// the first retry after a dead holder's lease lapses is granted.
const AGENT_TARGET_RESCUE_RETRY_MS = 3500;
// Claim the lease and act as the holder. A denied claim means another tab
// holds the lease. That holder can die before posting its result (reload,
// crash, even after renewing), and its lease lapses after ~3s, so this tab
// keeps retrying for as long as the server still holds the request: the
// answer's `pending` is the server's word that the request is alive, and
// it turns false the moment the request resolved or timed out, so no tab
// retries a request nobody awaits. A tab that turned busy meanwhile joins
// the roll call instead of taking a lease it cannot use. The first claim
// and the busy-to-idle re-claim share this.
function claimAndActOnAgentTarget(msg) {
if (agentTargetOverlayGone()) return;
const busy = agentTargetBusyReason(msg.targetId);
if (busy) { declineAgentTargetBusy(msg, busy); return; }
if (declineAgentTargetUnresolvable(msg)) return;
claimAgentTarget(msg.targetId, { eligible: true }).then((claim) => {
if (claim.granted) { noteAgentTarget(msg.targetId, 'acting'); actOnAgentTarget(msg); return; }
noteAgentTarget(msg.targetId, 'denied');
if (!claim.pending) return;
setTimeout(() => claimAndActOnAgentTarget(msg), AGENT_TARGET_RESCUE_RETRY_MS);
});
}
function retryDeclinedAgentTargets() {
if (busyDeclinedTargets.size === 0 || agentTargetBusyReason()) return;
for (const [targetId, msg] of busyDeclinedTargets) {
busyDeclinedTargets.delete(targetId);
claimAndActOnAgentTarget(msg);
}
}
// This page's participation in each target it heard: 'acting' once a
// claim was granted, 'done' once it replied (or stood down from a lapsed
// lease), else the word it last gave. The server replays pending targets
// to every connection that opens. After a reconnect that overlapped the
// old connection the server still holds this page's word; after one that
// did not, it dropped the word on the close, so a replayed target is
// handled again: a busy or unresolvable page re-declines (idempotent), an
// idle page claims.
const agentTargetsSeen = new Map();
function noteAgentTarget(targetId, status) {
agentTargetsSeen.set(targetId, status);
if (agentTargetsSeen.size > 100) agentTargetsSeen.delete(agentTargetsSeen.keys().next().value);
}
// A target this page took a lease on is off-limits for a replay: while
// acting (a second claim or Go), and once done, because its result may
// still be on the wire and this tab is GENERATING by then, so handling
// the replay would decline busy, hand the lease back mid-resolution, and
// let another tab fire a second Go.
function agentTargetTaken(targetId) {
const status = agentTargetsSeen.get(targetId);
return status === 'acting' || status === 'done';
}
// Only a page that can resolve the target claims it. A tab whose page
// lacks the element declines with its resolution verdict instead, so a
// first-wins claim never lets the wrong page answer for a target that
// another page has. The server prefers a busy report (a tab that could
// serve later) over these, and returns the resolution verdict only when
// no connected page can serve.
//
// An element can be momentarily absent (a route still rendering, an HMR
// commit mid-swap), so a failed resolution is not this page's final word:
// it is re-checked a few times over about two seconds, claiming the
// moment the element mounts, and only the last miss is reported. The
// server's timeout still bounds the whole exchange.
// The page reports the miss at once (so the other overlays' words can
// complete the roll call) and keeps re-checking at this cadence for as
// long as the server says the request is pending: the server holds an
// all-no_match roll call open for a short grace precisely so a late mount
// can still be claimed, drops the stale report on an eligible claim, and
// ends the watch by answering pending:false once the request resolved or
// timed out.
const AGENT_TARGET_RESOLVE_WATCH_MS = 500;
function declineAgentTargetUnresolvable(msg) {
const probe = resolveAgentTargetElement(msg);
if (!probe.error) return false;
reportAgentTargetUnresolvable(msg, probe.error);
return true;
}
function reportAgentTargetUnresolvable(msg, error) {
noteAgentTarget(msg.targetId, 'declined');
claimAgentTarget(msg.targetId, { eligible: false, state, reason: 'no_match', result: error }).then((answer) => {
if (!answer.pending) return;
setTimeout(() => watchAgentTargetResolution(msg, error), AGENT_TARGET_RESOLVE_WATCH_MS);
});
}
function watchAgentTargetResolution(msg, lastError) {
if (agentTargetOverlayGone() || agentTargetTaken(msg.targetId)) return;
const busy = agentTargetBusyReason(msg.targetId);
if (busy) { declineAgentTargetBusy(msg, busy); return; }
const probe = resolveAgentTargetElement(msg);
if (!probe.error) { claimAndActOnAgentTarget(msg); return; }
// Still unresolvable: re-report (idempotent); the answer says whether
// the server is still holding the request open.
reportAgentTargetUnresolvable(msg, probe.error || lastError);
}
function handleAgentTarget(msg) {
if (!msg || typeof msg.targetId !== 'string') return;
if (agentTargetTaken(msg.targetId)) return;
noteAgentTarget(msg.targetId, 'heard');
const busy = agentTargetBusyReason(msg.targetId);
if (busy) {
// Roll call: a busy tab reports itself and never acts. The server
// answers `busy` the moment every connected overlay has reported, so
// an idle tab elsewhere is never raced by a timer.
declineAgentTargetBusy(msg, busy);
return;
}
if (declineAgentTargetUnresolvable(msg)) return;
// Eligible tabs race for the server's lease and only the holder acts. A
// hidden tab yields a short head start so a visible one wins when both
// exist, and still serves the request on its own: the user finds the
// selection waiting when they return to it.
setTimeout(() => claimAndActOnAgentTarget(msg), document.hidden ? 150 : 0);
}
function actOnAgentTarget(msg) {
if (agentTargetOverlayGone()) return;
// Every exit ends this tab's acting state, so a later target is not
// refused for a Go that already happened or never will.
const reply = (result) => { noteAgentTarget(msg.targetId, 'done'); postAgentTargetResult(msg.targetId, result); };
const busy = agentTargetBusyReason(msg.targetId);
if (busy) {
// Turned busy between claim and act: report it, which also hands the
// lease back so the roll call can complete or a rescuer can claim.
declineAgentTargetBusy(msg, busy);
return;
}
const resolved = resolveAgentTargetElement(msg);
if (resolved.error) {
// The element went away between claim and act. A result would end the
// request for every tab; a decline hands the lease back so another
// page or a remount can still serve it.
reportAgentTargetUnresolvable(msg, resolved.error);
return;
}
const el = resolved.el;
if (msg.dryRun) {
reply({
ok: true,
dryRun: true,
matchCount: resolved.matchCount,
element: describeAgentTargetCandidate(el),
});
return;
}
scrollAgentTargetIntoView(el, () => {
// Torn down during the scroll settle: do not renew. The lease lapses
// for a rescuer instead of Go minting a session on a dismantled
// overlay.
if (agentTargetOverlayGone()) return;
// Renew the lease right before the irreversible part: a tab whose
// lease lapsed while it scrolled (a rescuer took over) stops here, so
// one request never gets two Go presses.
claimAgentTarget(msg.targetId, { eligible: true }).then((renewal) => {
if (!renewal.granted) { noteAgentTarget(msg.targetId, 'done'); return; }
// An insert placement left mid-configure gives way, exactly as a
// click outside it does in handleClick.
if (state === 'CONFIGURING' && configureKind === 'insert') cancelInsertConfigure();
// Mirror of the user-click pick entry in handleClick, minus the
// pick-mode gate (the agent's intent replaces the toggle); the entry
// goes through beginNewLiveConfiguration like every other pick so
// deferred recovery sees a fresh interaction revision.
selectedElement = el;
beginNewLiveConfiguration();
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
showBar('configure');
editBadgeSuppressed = true;
renderEditBadge('hidden');
startScrollTracking();
maybePrefetchPage();
maybeWarnConditionalAncestor(selectedElement);
// Preset what the agent asked for, then fire the same Go a user press
// fires. handleGo reads exactly these inputs.
selectedAction = msg.action;
selectedCount = msg.count;
// updateBarContent rebuilds the configure row and replaces the input
// element, so the prompt must be written into the input it creates,
// never before (the action-chip click handler does the same dance).
updateBarContent('configure');
const input = uiGetById(PREFIX + '-input');
if (input) input.value = msg.prompt || '';
// The target rides on the generate event too: the helper resolves
// the request from whichever lands first, so a page that dies
// between Go and its result cannot leave the request pending for a
// second Go elsewhere.
const candidate = describeAgentTargetCandidate(el);
agentTargetForGo = { targetId: msg.targetId, matchCount: resolved.matchCount, action: msg.action, count: msg.count, element: candidate };
handleGo();
agentTargetForGo = null;
if (state === 'GENERATING' && currentSessionId) {
reply({
ok: true,
matchCount: resolved.matchCount,
sessionId: currentSessionId,
action: msg.action,
count: msg.count,
element: candidate,
});
} else {
reply({ ok: false, error: 'go_failed', state });
}
});
});
}
// SSE (server→browser) + fetch POST (browser→server) // SSE (server→browser) + fetch POST (browser→server)
// Zero-dependency replacement for WebSocket. // Zero-dependency replacement for WebSocket.
// //
@@ -7121,7 +7538,7 @@
const SSE_MAX_RETRIES = 20; // generous: heartbeats keep the connection alive, so retries mean real trouble const SSE_MAX_RETRIES = 20; // generous: heartbeats keep the connection alive, so retries mean real trouble
function connectSSE() { function connectSSE() {
evtSource = new EventSource('http://localhost:' + PORT + '/events?token=' + TOKEN); evtSource = new EventSource('http://localhost:' + PORT + '/events?token=' + TOKEN + '&clientId=' + AGENT_TARGET_CLIENT_ID);
evtSource.onopen = () => { evtSource.onopen = () => {
sseRetries = 0; // reset on successful (re)connect sseRetries = 0; // reset on successful (re)connect
@@ -7132,8 +7549,11 @@
let msg; try { msg = JSON.parse(e.data); } catch { return; } let msg; try { msg = JSON.parse(e.data); } catch { return; }
switch (msg.type) { switch (msg.type) {
case 'connected': case 'connected':
applyLiveBarPreference(msg.hideLiveBar === true);
hasProjectContext = !!msg.hasProjectContext; hasProjectContext = !!msg.hasProjectContext;
if (!hasProjectContext) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000); // The generate lane runs without PRODUCT.md by design and never
// sends the user to init, so its quiet chrome skips this notice.
if (!hasProjectContext && !liveBarHiddenByHelper) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000);
console.log('[impeccable] Live mode connected.'); console.log('[impeccable] Live mode connected.');
syncAgentPollingUi(!!msg.agentPolling); syncAgentPollingUi(!!msg.agentPolling);
startAgentStatusPoll(); startAgentStatusPoll();
@@ -7143,9 +7563,15 @@
syncPageInteractionCursor(); syncPageInteractionCursor();
syncPageChatFocus('sse-connected'); syncPageChatFocus('sse-connected');
break; break;
case 'live_bar':
applyLiveBarPreference(msg.hidden === true);
break;
case 'agent_polling': case 'agent_polling':
syncAgentPollingUi(!!msg.connected); syncAgentPollingUi(!!msg.connected);
break; break;
case 'agent_target':
handleAgentTarget(msg);
break;
case 'agent_phase': case 'agent_phase':
if (msg.id === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) { if (msg.id === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) {
// Advance the visible phase monotonically. A behind/resumed // Advance the visible phase monotonically. A behind/resumed
@@ -7208,6 +7634,11 @@
disableInlineEdit(); disableInlineEdit();
refreshParamsPanel(); refreshParamsPanel();
} }
// The done reply is the agent's last word on this generation:
// with every variant mounted and no knobs declared, the Tune
// chip must stop spinning. A reload between the mount and this
// reply restored the pending state from the cache.
completeParameterGenerationIfReady();
break; break;
} }
// Source fallback when HMR did not land variants in this tab. // Source fallback when HMR did not land variants in this tab.
@@ -7371,6 +7802,15 @@
}).then(async res => { }).then(async res => {
if (res.ok) return res; if (res.ok) return res;
const body = await res.json().catch(() => ({})); const body = await res.json().catch(() => ({}));
// The helper refused to open a session for an agent target it has
// already answered (another page served it after this page's lease
// lapsed mid-capture, or the request timed out): drop the local
// session and hand the surface back.
if (body.error === 'agent_target_already_served' && msg.type === 'generate'
&& msg.id && msg.id === currentSessionId) {
abandonSupersededGo(msg.id);
return null;
}
// The server refused to journal progress for a session it has never // The server refused to journal progress for a session it has never
// seen: this browser is carrying state from another project or a // seen: this browser is carrying state from another project or a
// wiped store (two apps sharing a localhost port). Continuing to // wiped store (two apps sharing a localhost port). Continuing to
@@ -7392,6 +7832,14 @@
return sessionCreationGate.then(doSend); return sessionCreationGate.then(doSend);
} }
function abandonSupersededGo(sessionId) {
if (sessionId !== currentSessionId) return;
console.warn('[impeccable] The helper already answered this agent target; clearing session ' + sessionId + '.');
markSessionHandled();
cleanup({ instantChrome: true });
showToast('The helper already answered this request, so this session was cleared. Pick an element to start fresh.', 6000);
}
let abandonedForeignSessionId = null; let abandonedForeignSessionId = null;
function abandonForeignSession(sessionId) { function abandonForeignSession(sessionId) {
if (abandonedForeignSessionId === sessionId || sessionId !== currentSessionId) return; if (abandonedForeignSessionId === sessionId || sessionId !== currentSessionId) return;
@@ -7796,6 +8244,7 @@
visibleVariant = 0; visibleVariant = 0;
generationPhase = 'queued'; generationPhase = 'queued';
parameterGenerationState = 'pending'; parameterGenerationState = 'pending';
sessionOrigin = agentTargetForGo ? 'agent' : null;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
resetSessionFileMeta(); resetSessionFileMeta();
@@ -7821,6 +8270,24 @@
}; };
if (snapshot.comments.length > 0) basePayload.comments = snapshot.comments; if (snapshot.comments.length > 0) basePayload.comments = snapshot.comments;
if (snapshot.strokes.length > 0) basePayload.strokes = snapshot.strokes; if (snapshot.strokes.length > 0) basePayload.strokes = snapshot.strokes;
if (agentTargetForGo) {
// An agent-initiated Go names the target it serves (see
// actOnAgentTarget): the helper resolves that request from this event
// as well as from the overlay's own result post.
basePayload.agentTarget = {
targetId: agentTargetForGo.targetId,
clientId: AGENT_TARGET_CLIENT_ID,
result: {
ok: true,
matchCount: agentTargetForGo.matchCount,
sessionId: currentSessionId,
action: agentTargetForGo.action,
count: agentTargetForGo.count,
element: agentTargetForGo.element,
},
};
agentTargetForGo = null;
}
// Hide the interactive overlay so it doesn't linger during generation. // Hide the interactive overlay so it doesn't linger during generation.
hideAnnotOverlay(); hideAnnotOverlay();
@@ -7881,6 +8348,7 @@
visibleVariant = 0; visibleVariant = 0;
generationPhase = 'queued'; generationPhase = 'queued';
parameterGenerationState = 'pending'; parameterGenerationState = 'pending';
sessionOrigin = agentTargetForGo ? 'agent' : null;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
resetSessionFileMeta(); resetSessionFileMeta();
selectedElement = placeholderElement; selectedElement = placeholderElement;
@@ -8927,6 +9395,8 @@ void main() {
pagePickSkipClick = false; pagePickSkipClick = false;
currentSessionId = null; currentSessionId = null;
parameterGenerationState = 'idle'; parameterGenerationState = 'idle';
sessionOrigin = null;
editBadgeSuppressed = false;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
selectedAction = 'impeccable'; selectedAction = 'impeccable';
pendingAcceptedSession = null; pendingAcceptedSession = null;
@@ -9018,6 +9488,7 @@ void main() {
paramsCurrentValues = { ...saved.paramValues }; paramsCurrentValues = { ...saved.paramValues };
} }
if (saved.parameterState) parameterGenerationState = saved.parameterState; if (saved.parameterState) parameterGenerationState = saved.parameterState;
sessionOrigin = saved.origin === 'agent' ? 'agent' : null;
if (saved.generationPhase) generationPhase = saved.generationPhase; if (saved.generationPhase) generationPhase = saved.generationPhase;
} }
@@ -9105,7 +9576,12 @@ void main() {
} }
function restoreSessionWithoutWrapper(reason, activeSessions) { function restoreSessionWithoutWrapper(reason, activeSessions) {
const cached = loadSession(); // The session cache is per origin, so a tab on another page of the same
// app sees this page's session too. Only the page that saved it may
// resume it: the server-adoption branch below already applies the same
// check, and a tab on another page has nothing to render for it.
const cachedRaw = loadSession();
const cached = cachedRaw?.id && !pageMatchesCurrent(cachedRaw.pageUrl) ? null : cachedRaw;
// localStorage is a cache, not a gate. A cleared tab, a second browser // localStorage is a cache, not a gate. A cleared tab, a second browser
// profile, or a teardown that dropped local state all leave the durable // profile, or a teardown that dropped local state all leave the durable
// server session as the only record of work in progress; adopt it instead // server session as the only record of work in progress; adopt it instead
@@ -9218,6 +9694,7 @@ void main() {
pageUrl: location.pathname, pageUrl: location.pathname,
paramValues: { ...paramsCurrentValues }, paramValues: { ...paramsCurrentValues },
parameterState: parameterGenerationState, parameterState: parameterGenerationState,
origin: sessionOrigin || undefined,
insertPlaceholder: insertPlaceholderSnapshot || undefined, insertPlaceholder: insertPlaceholderSnapshot || undefined,
pickedAnchor: pickedAnchorSnapshot || undefined, pickedAnchor: pickedAnchorSnapshot || undefined,
pickedAnchorViewportTop: Number.isFinite(pickedAnchorViewportTop) ? pickedAnchorViewportTop : undefined, pickedAnchorViewportTop: Number.isFinite(pickedAnchorViewportTop) ? pickedAnchorViewportTop : undefined,
@@ -9343,6 +9820,8 @@ void main() {
pagePickSkipClick = false; pagePickSkipClick = false;
currentSessionId = null; currentSessionId = null;
parameterGenerationState = 'idle'; parameterGenerationState = 'idle';
sessionOrigin = null;
editBadgeSuppressed = false;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
selectedAction = 'impeccable'; selectedAction = 'impeccable';
renderEditBadge('hidden'); renderEditBadge('hidden');
@@ -9601,6 +10080,14 @@ void main() {
const resumedState = arrivedVariants > 0 ? 'CYCLING' : 'GENERATING'; const resumedState = arrivedVariants > 0 ? 'CYCLING' : 'GENERATING';
// A reload between the variants mounting and the agent's done reply
// restores a pending Tune state from the cache; the helper knows whether
// that generation already finished.
if (arrivedVariants >= expectedVariants && expectedVariants > 0
&& (parameterGenerationState === 'pending' || parameterGenerationState === 'loading')) {
settleParameterStateFromHelper(sessionId);
}
// Find the visible variant's content element for highlight positioning. // Find the visible variant's content element for highlight positioning.
const isInsert = wrapper.dataset.impeccableMode === 'insert'; const isInsert = wrapper.dataset.impeccableMode === 'insert';
const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null; const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null;
@@ -11065,6 +11552,21 @@ void main() {
} }
} }
// After a resume the cache may say the Tune knobs are still coming while
// the agent already replied done before the reload. The helper's session
// record settles it; otherwise the done reply on SSE does.
function settleParameterStateFromHelper(sessionId) {
fetch('http://localhost:' + PORT + '/status?token=' + TOKEN, { cache: 'no-store' })
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
if (!data || sessionId !== currentSessionId) return;
const session = (data.activeSessions || []).find((s) => s && s.id === sessionId);
if (!session) return;
if (session.generationCompletedAt || session.generationPhase === 'completed') completeParameterGenerationIfReady();
})
.catch(() => { /* the done reply on SSE settles it otherwise */ });
}
function fetchAgentPollingStatus() { function fetchAgentPollingStatus() {
fetch('http://localhost:' + PORT + '/status?token=' + TOKEN, { cache: 'no-store' }) fetch('http://localhost:' + PORT + '/status?token=' + TOKEN, { cache: 'no-store' })
.then((res) => (res.ok ? res.json() : null)) .then((res) => (res.ok ? res.json() : null))
@@ -11104,11 +11606,15 @@ void main() {
uiAppendStyle(s); uiAppendStyle(s);
} }
// The generate lane's helper says so in the served script itself, so a
// lane session never draws the bar at all; every other session mounts
// it exactly as before.
const barHiddenFromStart = window.__IMPECCABLE_LIVE_BAR_HIDDEN__ === true;
globalBarEl = el('div', { globalBarEl = el('div', {
position: 'fixed', bottom: '14px', left: '50%', position: 'fixed', bottom: '14px', left: '50%',
transform: 'translateX(-50%) translateY(20px)', transform: 'translateX(-50%) translateY(20px)',
zIndex: Z.bar + 5, zIndex: Z.bar + 5,
display: 'flex', alignItems: 'stretch', display: barHiddenFromStart ? 'none' : 'flex', alignItems: 'stretch',
gap: '0', gap: '0',
width: 'max-content', width: 'max-content',
background: P.surface, background: P.surface,
@@ -11124,6 +11630,10 @@ void main() {
}); });
globalBarEl.id = PREFIX + '-global-bar'; globalBarEl.id = PREFIX + '-global-bar';
globalBarEl.dataset.theme = theme; globalBarEl.dataset.theme = theme;
if (barHiddenFromStart) {
liveBarHiddenByHelper = true;
globalBarEl.dataset.liveBarDisplay = 'flex';
}
// Brand mark - kinpaku Impeccable icon (site header / favicon paths). // Brand mark - kinpaku Impeccable icon (site header / favicon paths).
const brand = el('span', { const brand = el('span', {
@@ -11519,6 +12029,9 @@ void main() {
// Listen for detection results AND ready signal // Listen for detection results AND ready signal
window.addEventListener('message', onDetectMessage); window.addEventListener('message', onDetectMessage);
updateGlobalBarState(); updateGlobalBarState();
// The helper may already have said the bar stays hidden (a connect
// that raced the bar build, or a reload mid-lane): re-apply it here.
if (liveBarHiddenByHelper) setLiveBarHidden(true);
} }
function updateGlobalBarState() { function updateGlobalBarState() {
@@ -11715,6 +12228,13 @@ void main() {
/** Full teardown: remove all UI, disconnect SSE, clean up. */ /** Full teardown: remove all UI, disconnect SSE, clean up. */
function teardown() { function teardown() {
// Declined targets die with the overlay: the IDLE transition below must
// not re-claim a lease this page can no longer act on. So does the
// target ledger: an 'acting' entry from a Go that never happened must
// not refuse every target the next connection hears.
busyDeclinedTargets.clear();
agentTargetsSeen.clear();
liveBarHiddenByHelper = false;
stopAgentStatusPoll(); stopAgentStatusPoll();
hideAgentPollTooltip(); hideAgentPollTooltip();
if (agentPollTooltipEl) { if (agentPollTooltipEl) {
-29
View File
@@ -1,29 +0,0 @@
{
"hooks": {
"BeforeTool": [
{
"matcher": "^run_shell_command$",
"hooks": [
{
"name": "impeccable-session",
"type": "command",
"command": "[ ! -f \"$GEMINI_PROJECT_DIR/.gemini/skills/impeccable/scripts/impeccable\" ] || \"$GEMINI_PROJECT_DIR/.gemini/skills/impeccable/scripts/impeccable\" hook",
"timeout": 5000
}
]
}
],
"AfterAgent": [
{
"hooks": [
{
"name": "impeccable-completion",
"type": "command",
"command": "[ ! -f \"$GEMINI_PROJECT_DIR/.gemini/skills/impeccable/scripts/impeccable\" ] || \"$GEMINI_PROJECT_DIR/.gemini/skills/impeccable/scripts/impeccable\" hook",
"timeout": 30000
}
]
}
]
}
}
+3 -2
View File
@@ -1,7 +1,7 @@
--- ---
name: impeccable name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks. description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.4.0 version: 4.3.1
--- ---
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft. This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
@@ -62,7 +62,8 @@ Choose the mode from the requested surface, not the product, and persist it only
| `clarify [target]` | Fix | Improve UX copy, labels, and error messages | [reference/clarify.md](reference/clarify.md) | | `clarify [target]` | Fix | Improve UX copy, labels, and error messages | [reference/clarify.md](reference/clarify.md) |
| `adapt [target]` | Fix | Adapt for different devices and screen sizes | [reference/adapt.md](reference/adapt.md) · native: [reference/adapt.native.md](reference/adapt.native.md) | | `adapt [target]` | Fix | Adapt for different devices and screen sizes | [reference/adapt.md](reference/adapt.md) · native: [reference/adapt.native.md](reference/adapt.native.md) |
| `optimize [target]` | Fix | Diagnose and fix UI performance | [reference/optimize.md](reference/optimize.md) | | `optimize [target]` | Fix | Diagnose and fix UI performance | [reference/optimize.md](reference/optimize.md) |
| `live` | Iterate | Visual variant mode: pick elements in the browser, generate alternatives | [reference/live.md](reference/live.md) | | `live` | Iterate | Visual variant mode: pick elements in the browser, iterate on alternatives | [reference/live.md](reference/live.md) |
| `generate [n] [action] [element]` | Iterate | Variants, versions, or alternatives of a named element to choose from in the live browser; no manual picking | [reference/generate.md](reference/generate.md) |
Routing: Routing:
@@ -188,6 +188,12 @@ Test thoroughly across contexts:
- **Edge cases**: Very small screens (320px), very large screens (4K) - **Edge cases**: Very small screens (320px), very large screens (4K)
- **Slow connections**: Test on throttled network - **Slow connections**: Test on throttled network
**Custom controls** (sliders, drag surfaces, scrollable control strips): a before/after slider can pass every width check above and still refuse to drag on iOS, so exercise each one in scope in the same batched round as the checks above:
- **Primary gesture**: Tap it and confirm it responds as designed, then drag it with the target input method; the drag must complete, not just start
- **Scroll across it**: A swipe along the page's scroll axis across the control scrolls the page or container without activating it; a drag that starts on the control along its axis moves the control, not the page. Neither failure throws an error, so try both
- **Evidence**: Say what produced the evidence: an emulated viewport, synthesized touch input through a browser tool, which engine ran it (Chromium is not Safari), or a physical device. Screenshots and resized viewports verify layout, never a gesture. Name what stayed untested and move on; unreachable hardware is a reported gap, not a blocker
When the adaptation feels native to each context, hand off to `/impeccable polish` for the final pass. When the adaptation feels native to each context, hand off to `/impeccable polish` for the final pass.
--- ---
+2 -1
View File
@@ -48,11 +48,12 @@ Run comprehensive checks across 5 dimensions. Score each dimension 0-4 using the
**Check for**: **Check for**:
- **Fixed widths**: Hard-coded widths that break on mobile - **Fixed widths**: Hard-coded widths that break on mobile
- **Touch targets**: Interactive elements < 44x44px - **Touch targets**: Interactive elements < 44x44px
- **Broken touch interaction**: Custom sliders, drag surfaces, and scrollable control strips whose primary gesture fails under touch, that swallow page scroll or lose the drag to it, or that stay stuck after an interrupted gesture. Code tells: mouse-only handlers, no `touch-action` on a pointer-event drag surface, drag state that nothing clears on cancel, lost capture, or blur. Exercise the gesture when a browser tool can synthesize touch (a rendered viewport proves layout, not the gesture), then say what produced the evidence (emulated viewport, synthesized touch, which engine, physical device) and what stayed untested
- **Horizontal scroll**: Content overflow on narrow viewports - **Horizontal scroll**: Content overflow on narrow viewports
- **Text scaling**: Layouts that break when text size increases - **Text scaling**: Layouts that break when text size increases
- **Missing breakpoints**: No mobile/tablet variants - **Missing breakpoints**: No mobile/tablet variants
**Score 0-4**: 0=Desktop-only (breaks on mobile), 1=Major issues (some breakpoints, many failures), 2=Partial (works on mobile, rough edges), 3=Good (responsive, minor touch target or overflow issues), 4=Excellent (fluid, all viewports, proper touch targets) **Score 0-4**: 0=Desktop-only (breaks on mobile), 1=Major issues (some breakpoints, many failures), 2=Partial (works on mobile, rough edges), 3=Good (responsive, minor touch target or overflow issues), 4=Excellent (fluid, all viewports, proper touch targets, gestures work under touch)
### 5. Implementation Integrity (CRITICAL) ### 5. Implementation Integrity (CRITICAL)
@@ -1,57 +0,0 @@
# Component review
Use this checkpoint on comp-led builds after producing the initial component kit and before composing the page. The approved comp is the reference. The user reviews the actual produced components, including code; a list of planned assets or screenshots supplied by the builder is not a review of what will ship.
## Prepare the component kit
Keep the measured spec's region IDs. Include every visible region: produced raster assets and working HTML/CSS/SVG for text, controls, patterns, decoration and layout elements. A region rendered in code needs an actual review document, not a promise to implement it later. Use semantic HTML for content and controls. Do not flatten the page or combine unrelated regions to avoid review. Report omitted regions so the user can mark what is missing.
Write `.impeccable/review/components.json` with this manifest format:
```json
{
"schemaVersion": 1,
"id": "components",
"title": "Component review",
"stage": "components",
"comp": {"path": ".impeccable/mocks/comp-2.png", "width": 1536, "height": 1024},
"components": [
{
"id": "illustration",
"name": "Illustration",
"medium": "raster",
"box": {"x": 0.5, "y": 0.2, "w": 0.45, "h": 0.7},
"note": "Produced cutout; positioned over the page ground.",
"preview": {"kind": "image", "path": "assets/illustration.png"},
"dependencies": [".impeccable/build/spec.json"]
},
{
"id": "headline",
"name": "Headline",
"medium": "html",
"box": {"x": 0.05, "y": 0.2, "w": 0.4, "h": 0.25},
"note": "Rendered semantic heading and its typography.",
"preview": {"kind": "page", "path": ".impeccable/review/components/headline.html"},
"dependencies": [".impeccable/build/spec.json", "assets/type.woff2"]
}
]
}
```
The coordinates above only illustrate the schema. Use the approved comp's actual pixel dimensions and each measured region's normalized bounds (`x / width`, `y / height`, `w / width`, `h / height`). A code preview is rendered at the comp viewport and cropped to that component's box, so place its content at those coordinates in the review document. Include every file the document uses in `dependencies`, including linked CSS, fonts and images. The runtime also binds the measured spec for the component stage and checks its inventory. Local paths only. Static PNG, WebP and JPEG previews retain their original bytes and actual transparency; never draw a checkerboard into the asset.
Native capture supports stable HTML/CSS and inline SVG. Supply a static review state for motion and keep the implementation's real inputs. A scripted, canvas or otherwise unsupported component is a blocker to report, not permission to substitute a raster or omit it.
## Present and wait
If the harness exposes `component_review`, call it with `manifest_path` set to `.impeccable/review/components.json`. The host captures the component files, presents this same review interface and returns the user's decisions. A suspended request is waiting for the user; it is not a failed build or an approval.
Otherwise run `.gemini/skills/impeccable/scripts/impeccable component-review capture --manifest .impeccable/review/components.json`, then start `.gemini/skills/impeccable/scripts/impeccable component-review serve --session <returned session>` in the background. Open the URL it prints in the available browser and wait for the user. Read the result with `.gemini/skills/impeccable/scripts/impeccable component-review verify --manifest .impeccable/review/components.json`; pending, needs-work and stale input all refuse approval. Never submit the page or write a receipt on the user's behalf.
The user can approve components, request changes, and mark missing regions. Act on their feedback without replacing it with your own favorable verdict. Keep component IDs stable, update the actual implementation and dependency list, and present another round. The UI carries only approvals whose component inputs have not changed. Do not ask the user to reapprove unchanged work. Continue only when the inventory is confirmed and all components are approved.
## Assemble and review
Build the page from the approved component files. Replacing, simplifying or changing an approved component requires a new component review. Run the existing plates and hero gates; human review does not waive their integrity checks.
After the full page and responsive checks are complete, present a second manifest at `.impeccable/review/hero.json`, with `id` and `stage` set to `hero`. Use one page-preview component covering the assembled first viewport, its real HTML entry, and its complete dependency list. The reference stays the approved comp. Call the same host review tool (or native capture/serve/verify workflow) and obtain the user's approval before the final response. Later edits to the reviewed files require a fresh review. A component-kit approval does not approve their assembled layout.
@@ -13,10 +13,6 @@ Do not redesign. Preserve the reference's visual role, silhouette, palette, ligh
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 `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; 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; report a card too thin to brief a comp, never pad it 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 (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 keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. 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. 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 `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; 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; report a card too thin to brief a comp, never pad it 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 (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 keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. 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.
## Review handoff
After the initial production batch, return the actual files and any unresolved drift to the parent for the user-facing component review in [component-review.md](../reference/component-review.md). The parent includes rendered code regions alongside these rasters. A parent or automatic visual check is not a substitute for that human checkpoint. Apply requested repairs and preserve unchanged assets; do not self-approve them. This checkpoint does not apply to the Decision Comps job above.
## Input Contract ## Input Contract
Expect the measured spec (`.impeccable/build/spec.json`, written by `impeccable comp-spec` from the approved comp), the approved comp path, and the skill scripts path. Optionally: a subset of region ids to produce, extra prompt notes per region, and format or transparency needs. Everything else you need is in the spec: each raster region's id, kind (plate, image, texture), pixel box, sampled palette, aspect, note, and the plate path it must land on. Expect the measured spec (`.impeccable/build/spec.json`, written by `impeccable comp-spec` from the approved comp), the approved comp path, and the skill scripts path. Optionally: a subset of region ids to produce, extra prompt notes per region, and format or transparency needs. Everything else you need is in the spec: each raster region's id, kind (plate, image, texture), pixel box, sampled palette, aspect, note, and the plate path it must land on.
@@ -16,7 +16,7 @@ Expect: the original request; the confirmed user answers; the artifact path(s);
## Checks, in order ## Checks, in order
0. **Evidence.** Before any other check, verify the required captures exist and every capture is valid. Required: the platform's full viewport set (web: `desktop.png` and `mobile.png`; native: one capture per shipped device class), plus every capture the calling brief names as required, a reported user viewport (`user-<width>.png`) included. Valid: no black or blank regions, content matching what the filename claims (a visit capture showing the About section is invalid), the document top visible where the file claims a full page, dimensions that make sense for the named viewport. A required capture that is absent fails exactly like one that is malformed: a viewport nobody captured is a viewport nobody inspected, and it cannot ship. When any capture fails, the whole review changes shape: return `disposition: recapture` as the first line, then one section, `recapture`, listing each missing or invalid file and what a valid capture of it shows, and stop. Never build a matrix on malformed evidence; a verdict derived from a broken capture launders the breakage into an approval, and the parent owes you a full re-review on valid captures, not a scoring round. 0. **Evidence.** Before any other check, verify the required captures exist and every capture is valid. Required: the platform's full viewport set (web: `desktop.png` and `mobile.png`; native: one capture per shipped device class), plus every capture the calling brief names as required, a reported user viewport (`user-<width>.png`) included. Valid: no black or blank regions, content matching what the filename claims (a visit capture showing the About section is invalid), the document top visible where the file claims a full page, dimensions that make sense for the named viewport. A required capture that is absent fails exactly like one that is malformed: a viewport nobody captured is a viewport nobody inspected, and it cannot ship. When any capture fails, the whole review changes shape: return `disposition: recapture` as the first line, then one section, `recapture`, listing each missing or invalid file and what a valid capture of it shows, and stop. Never build a matrix on malformed evidence; a verdict derived from a broken capture launders the breakage into an approval, and the parent owes you a full re-review on valid captures, not a scoring round.
1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and every phase before `review` is `closed` or explicitly `skipped`; an open or failed phase is a material finding. A comp-led config with no state file, or a state whose `comps` phase is neither `closed` nor `skipped`, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. 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, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all. 1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and its `comps` (or `skipped` when a surface round locked the comp), `spec`, `plates`, and `hero` phases are `closed`; a comp-led config with no state file, or a state whose `comps` phase never closed, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. 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, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all.
2. **Fidelity.** Start from the measurement, then judge what it cannot: read `.impeccable/review/diff/final/report.json` (and hero) first; every region scored `missing` or `contradicted` is a matrix row in that state unless the paired crop under `regions/` shows the score is wrong, and you say why; a region scored `match` still gets your eye for lettering character and material, which the numbers do not measure. Then, 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; 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. Three 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; medium is part of the promise. GROUND: the page field's value and temperature against the comp's, sampled from pixels on both sides when tooling allows rather than judged from memory, and read as the net on-screen result where a texture or tile paints over the base color; a ground warmer or cooler than the comp's is contradicted however faithfully the layout matches, and drift toward the rendition prior (warm cream on light grounds, blue-black slate on dark) is the direction to hunt. With no approved comp, 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 renders) as contradicted on its face; imitation material is the single most reliable mark of machine-made design. GROUND narrows rather than lapses: with no comp to sample, a color OWN-WORLD names is the target and the same warmer-or-cooler judgment applies; when OWN-WORLD names none, there is no GROUND authority, and the review says so in place of a verdict, because a target the reviewer invents turns the check into taste. A critique-reference comp on such a build is provocation, not spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is what the image dared that the build did not, and 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. A fix that requires producing an asset says so explicitly ("produce: <region> 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. 2. **Fidelity.** Start from the measurement, then judge what it cannot: read `.impeccable/review/diff/final/report.json` (and hero) first; every region scored `missing` or `contradicted` is a matrix row in that state unless the paired crop under `regions/` shows the score is wrong, and you say why; a region scored `match` still gets your eye for lettering character and material, which the numbers do not measure. Then, 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; 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. Three 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; medium is part of the promise. GROUND: the page field's value and temperature against the comp's, sampled from pixels on both sides when tooling allows rather than judged from memory, and read as the net on-screen result where a texture or tile paints over the base color; a ground warmer or cooler than the comp's is contradicted however faithfully the layout matches, and drift toward the rendition prior (warm cream on light grounds, blue-black slate on dark) is the direction to hunt. With no approved comp, 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 renders) as contradicted on its face; imitation material is the single most reliable mark of machine-made design. GROUND narrows rather than lapses: with no comp to sample, a color OWN-WORLD names is the target and the same warmer-or-cooler judgment applies; when OWN-WORLD names none, there is no GROUND authority, and the review says so in place of a verdict, because a target the reviewer invents turns the check into taste. A critique-reference comp on such a build is provocation, not spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is what the image dared that the build did not, and 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. A fix that requires producing an asset says so explicitly ("produce: <region> 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. 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, 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. 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, 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.
@@ -0,0 +1,101 @@
> **Additional context needed**: only the target element, when the request does not name one that resolves uniquely on the page.
Generate is the fast lane into live mode: the user names an element, a direction, and a count in one sentence, and within a minute they are cycling through variants in their browser. One command boots the helper, hands the element to the overlay in the page your harness already shows (it scrolls to it, selects it, and fires the same Go a click fires) and returns the generate event; one edit writes the variants; one call replies and waits for the user's choice, which the helper bakes into source itself. This file owns the lane's plumbing; from the event onward the design work is [live.md](live.md)'s, unchanged, so read it in full now if you have not this session.
**Web only.** Live mode's browser overlay has no native equivalent; on `ios` / `android` / `adaptive` projects, decline this command and offer `bolder` or `quieter` on the source instead.
The plumbing is where the lane saves time: one command starts the session around the page your harness already shows, one call replies and waits, and nothing here is a browser you have to babysit. The design work is not where it saves time. Setup runs as for any command (`impeccable context`, this reference, craft-floor.md before the edit), and the variants are planned, written, and accepted exactly the way a live session plans, writes, and accepts them.
Three prohibitions cover the known ways this command goes wrong:
- **Never run init or document, and never ask for PRODUCT.md or DESIGN.md.** When they exist, the start command prints them under `boot` and you use them. When they do not, it says so (`contextMissing`, `contextNote`) and you extract the identity from the event (Step 3). A missing file is never a reason to interview the user inside this command; offer `init` in one line after the session ends.
- **Never hand-write a variants wrapper or invent a session id.** Only the browser mints session ids (8 hex characters, at Go). A missing event is fixed by rerunning Step 2, never with a direct source edit.
- **Do not act on hook findings while live markers are in the file**, and do not restyle variants to appease them; the accept verifies the file once the variant is permanent.
## Step 1: Parse the request
Three parts, all from the user's sentence:
- **A number in the request**: that is the count. **No number**: 3. The protocol caps count at 8.
- **The direction wording** maps onto the live action vocabulary; never invent a new action value:
- **bold, bolder, stronger, punchier**: `bolder`
- **quiet, calmer, softer, toned down**: `quieter`
- **simpler, minimal, stripped**: `distill`
- **refined, tightened, polished**: `polish`
- **font and type words**: `typeset`
- **color words**: `colorize`
- **arrangement and spacing words**: `layout`
- **device and breakpoint words**: `adapt`
- **motion words**: `animate`
- **playful words**: `delight`
- **rule-breaking words**: `overdrive`
- **Wording that carries intent but no vocabulary word** ("make it feel like a bank", "warmer", "more premium"): `impeccable`, with the user's wording passed as the prompt.
- **An action fits AND extra intent rides along** ("bolder, but keep it monochrome"): that action, with the rest as the prompt.
- **The wording names no direction at all** ("better", "improve", "nicer", "different", "fresh", "new", "redesign", "fix", "some options", "ideas", "alternatives", or just "variants" with nothing else): Ask the user directly to clarify what you cannot infer. Ask one question, offering the vocabulary: *"Which direction should the variants take? bolder, quieter, simpler (distill), polished, typography (typeset), color (colorize), layout, motion (animate), playful (delight), or rule-breaking (overdrive)."* Map the answer with this list; an answer that is still open ("surprise me", "you pick") is `impeccable` with the user's original wording as the prompt, and Step 2 starts on that answer.
- **The element description** ("the pricing cards", "the hero heading"): Step 2 resolves it to a selector.
Done when you hold an action from the vocabulary (asked for, when the request named no direction), a count from 1 to 8, and the element description.
## Step 2: Reuse the page, then start
**Reuse** the dev server already running and the tab your harness already shows it in; a second server or a second browser window is the failure this step prevents.
1. **Find the dev server**, cheapest source first, and stop at the first hit: the user's message, a browser tab already on the app (Claude Code: an origin in `tabs_context`), a server your harness started (Claude Code: `preview_list`), a terminal that printed its URL. Its origin is your `--dev-url`. **No hit**: leave `--dev-url` off and run the start command with no wait; the boot probes for a running server and its verdict names the move. `browser_needed` carries the `devUrl` it found: open it as in 2, then rerun with `--dev-url <devUrl> --wait-for-browser 60000`. `no_dev_server` means nothing serves the app: start the dev script the way the verdict says (Claude Code: `preview_start`; Cursor: a background terminal; Codex: an exec you yield from), wait for its URL, then rerun with `--dev-url <url>`.
2. **Open the page that renders the element in your browser, then start.** The route the request names, else the one `--target` serves; `--dev-url` takes only the origin.
- **Cursor** (`browser_navigate`) and **Claude Code** (`navigate`, which opens the Browser pane when it is closed and takes the `tabId` from `tabs_context` when a tab is already on that origin): open the URL, then run the start command with `--dev-url <url> --wait-for-browser 60000`. The boot injects the overlay and the page reloads into it while the command waits. Your browser tool is the only opener on these harnesses; the engine ignores `--open` there.
- **No browser tool** (Codex, others): run the start command with `--open --wait-for-browser 120000`; it opens the system browser, and the longer wait covers the user finding the tab. **`browser_open_failed` back**: tell the user the `url` in one line and rerun with `--wait-for-browser 120000`.
```bash
.gemini/skills/impeccable/scripts/impeccable live-generate --target src/App.jsx --dev-url http://127.0.0.1:5173/ --selector ".pricing-grid" --action bolder --count 3 --boot --wait-for-browser 60000
```
Run it in the foreground in Cursor and Claude Code (it returns within the wait); on Codex, in an exec you yield from, the way Step 3 runs the poll.
- `--target`: the file that renders the element when the request or the project makes it obvious; skip it otherwise.
- `--dev-url`: the origin from 1; omit it and the boot probes.
- `--selector`: a unique class first, then a landmark tag plus class, an id last (every variant mounts a copy of the element, so an id repeats in the DOM). **The request names a repeated component in plural** ("the pricing cards"): target the container that holds the set, so one scoped stylesheet restyles every instance. One read of the source file that renders the element is allowed when the selector is not obvious; `--dry-run` resolves and reports without starting anything when it is not certain.
- `--boot`: runs the lane's boot (PRODUCT.md and DESIGN.md loaded again for the helper, missing files tolerated, dev URL found, bottom bar hidden for the helper's lifetime) and reuses a helper that is already running. Its result rides along as `boot`.
- Also available: `--prompt`, `--text` (keep only matches whose visible text contains a snippet), `--index` (1-based pick among matches).
Read the output in this order: `boot` (or `boot.contextMissing` with `boot.contextNote`: the page is the source of truth, per the note), then `event`, the generate event for `sessionId`, with the same `_instructions` a user's Go gets. Every verdict carries `_instructions`, and they win over your recollection of this file; the ones whose move is a decision of yours:
- **`ambiguous`**: the candidates are listed; target their common container, or rerun with `--text "<visible text>"` or `--index <n>`.
- **`dev_server_gone`**: the dev server stopped answering while the command waited for the page (on Cursor, a server another chat started dies with that chat). Start it the way the verdict says, then rerun with `--dev-url <url>`.
- **`no_match`**: the tab is on a route that does not render the element (navigate to the right route, rerun), or the selector is wrong (derive a better one from the source, or add `--text`).
- **`config_missing` / `config_invalid`** under `bootError`: follow [live-setup.md](live-setup.md) first, then rerun.
- **`event: null`** with `ok: true`: the event was slower than the wait; run `.gemini/skills/impeccable/scripts/impeccable live-poll` once to collect it, then continue.
Done when the output shows `ok: true`, a `sessionId`, and an `event`, reached with at most one server started and one tab opened by you.
## Step 3: Generate
The event is a standard `generate` event: the picked element's context, a preflighted scaffold, and `_instructions` naming the action's reference, the planning section, and the exact splice. Handle it exactly per live.md's **Handle generate**, which owns everything from the identity lock to the done reply: read the action's reference and craft-floor.md as it says, plan per section 4 (identity first, then mode, then three different primary axes, then the squint test), declare knobs per section 7, and deliver per section 6 (a complete replacement of the element per variant, the preview CSS plus every variant in one edit at the scaffold's splice). The lane changes nothing about what a variant may be: the moves a live session would make on this element (a promoted tier, a restructured set, a reordered card, a different surface) are open here too. Never screenshot the page; the overlay preview is the review channel until accept.
**Reply and wait in one call**, with the file you wrote:
```bash
.gemini/skills/impeccable/scripts/impeccable live-poll --reply EVENT_ID done --file src/App.jsx --then-poll
```
This replies done (the browser mounts the variants) and then blocks until the user's choice arrives, so run it the way your harness runs a long wait: **Claude Code** in the foreground with your tool's longest timeout (600000 ms), so you are paused until the choice arrives; **Codex** in a yielded foreground exec; **Cursor** in a background terminal with notify on `"type":"(accept|discard|variant_mount_failed|exit)"`. Never pass a short `--timeout=`. While it runs there is nothing else to do: never sleep and never poll its output on a timer; a harness that backgrounds it wakes you when it returns. `{"type":"timeout"}` means the user has not chosen yet: run `live-poll` again and keep waiting. If the edit fails after the browser flipped to GENERATING, `--reply EVENT_ID error "Short reason"` (without `--then-poll`) so the bar resets.
Then tell the user, in one line, where their variants are: *"Three [bolder] variants are live on [the pricing cards]: cycle with the floating bar's arrows, adjust the Tune knobs, and Accept the keeper."*
Outside the replace path, read the matching live.md section before acting: `scaffold.previewMode: "svelte-component"` (Svelte previews are edited as components, and their accept is mechanical), `mode: "insert"`, `variant_mount_failed`, `steer`, `manual_edit_apply`, and any `fallback: "agent-driven"` wrap error.
## Step 4: Accept and close
The call from Step 3 returns the user's choice. **`discard`**: nothing to do. **`accept`**: `_acceptResult.carbonize: true` is the normal case, and the cleanup is live.md's **Required after accept**, unchanged: move the accepted variant's rules into the stylesheet that already owns the element with real selectors, bake the chosen knob values in, unwrap the element and drop every `data-impeccable-*` attribute, delete the inline `<style>` block and both `impeccable-carbonize` markers, then `.gemini/skills/impeccable/scripts/impeccable live-complete --id SESSION_ID` and confirm `phase: "completed"`. (`baked: true` appears only when the accept was run with `--bake`; then the helper already made the variant permanent and no `live-complete` is owed.)
Close without being asked, the moment the choice is handled:
```bash
.gemini/skills/impeccable/scripts/impeccable live-server stop
```
Stopping removes the injected script and reloads the page once: the user sees the accepted design with no overlay chrome, still served by their dev server. **Never kill or restart the dev server**, including one you started in Step 2.
- **The user asks for more variants before you closed**: skip the close, run Step 2 again for the next element (the helper is reused), and close after the last choice.
- **Interrupted or unsure of the state**: `.gemini/skills/impeccable/scripts/impeccable live-status`, then `live-resume`; the journal under `.impeccable/live/sessions/` is canonical.
Done when the helper is stopped and the dev site still answers with the accepted design.
@@ -205,6 +205,11 @@ t('items', { count }) // Handles complex plural rules
- Optimistic updates with rollback - Optimistic updates with rollback
- Conflict resolution - Conflict resolution
**Interrupted gestures** (custom sliders, drag surfaces, scrollable control strips):
- A second finger or pointer lands mid-drag: the first drag keeps its pointer or ends cleanly, never jumps to the new one
- The browser cancels the gesture to scroll (`pointercancel`), capture is lost (`lostpointercapture`), the pointer is released outside the control, or the window loses focus (`blur`) mid-drag: clear the dragging state and release capture
- After each of these, the next tap or drag works without a reload
**Permission states**: **Permission states**:
- No permission to view - No permission to view
- No permission to edit - No permission to edit
@@ -304,6 +309,7 @@ const throttledScroll = throttle(handleScroll, 100);
- Unit tests for edge cases - Unit tests for edge cases
- Integration tests for error scenarios - Integration tests for error scenarios
- E2E tests for critical paths - E2E tests for critical paths
- A behavioral regression for each confirmed gesture fix, when the project's test runner can drive input
- Visual regression tests - Visual regression tests
- Accessibility tests (axe, WAVE) - Accessibility tests (axe, WAVE)
@@ -330,7 +336,10 @@ Test thoroughly with edge cases:
- **Network issues**: Disable internet, throttle connection - **Network issues**: Disable internet, throttle connection
- **Large datasets**: Test with 1000+ items - **Large datasets**: Test with 1000+ items
- **Concurrent actions**: Click submit 10 times rapidly - **Concurrent actions**: Click submit 10 times rapidly
- **Interrupted gestures**: Add a second finger mid-drag, scroll across the control, release outside it, switch windows mid-drag; then drag again
- **Errors**: Force API errors, test all error states - **Errors**: Force API errors, test all error states
- **Empty**: Remove all data, test empty states - **Empty**: Remove all data, test empty states
For gestures, say what produced the evidence (emulated viewport, synthesized touch, which engine, physical device) and name what stayed untested.
When edge cases are covered, hand off to `/impeccable polish` for the final pass. When edge cases are covered, hand off to `/impeccable polish` for the final pass.
@@ -96,7 +96,7 @@ Build the assigned direction, not a safer interpretation of it. The form supplie
When an approved comp exists, it is a spatial contract, not a mood board: only the user can downgrade its authority, in explicit words. Models systematically believe their HTML, CSS, and SVG recreation of an image succeeded when it did not, so the build runs as a state machine on disk whose gates measure the screen against the comp instead of asking you to remember it. Start it once, and let it tell you what is next: When an approved comp exists, it is a spatial contract, not a mood board: only the user can downgrade its authority, in explicit words. Models systematically believe their HTML, CSS, and SVG recreation of an image succeeded when it did not, so the build runs as a state machine on disk whose gates measure the screen against the comp instead of asking you to remember it. Start it once, and let it tell you what is next:
`.gemini/skills/impeccable/scripts/impeccable build-phase start --direction <seed key> --kind <assigned|pick|challenger|canon> --artifact <entry file>` right after the direction choice (this is also the choice ping; the roll's output names the exact command), or `start --comp <approved comp> --artifact <entry file>` when a surface round already locked one. `.gemini/skills/impeccable/scripts/impeccable build-phase start --direction <seed key> --kind <assigned|pick|challenger|canon>` right after the direction choice (this is also the choice ping; the roll's output names the exact command), or `start --comp <approved comp>` when a surface round already locked one.
Then, in order, each closed by `.gemini/skills/impeccable/scripts/impeccable build-phase advance` (every verb below runs as `.gemini/skills/impeccable/scripts/impeccable <verb>`; exit 2 means the gate failed and printed why; fix that and advance again; write nothing for a later phase while an earlier gate is open): Then, in order, each closed by `.gemini/skills/impeccable/scripts/impeccable build-phase advance` (every verb below runs as `.gemini/skills/impeccable/scripts/impeccable <verb>`; exit 2 means the gate failed and printed why; fix that and advance again; write nothing for a later phase while an earlier gate is open):
@@ -104,9 +104,8 @@ Then, in order, each closed by `.gemini/skills/impeccable/scripts/impeccable bui
The comp-led path is a frontier-tier job: it asks the builder to hold a measured layout, place plates at their boxes, and act on numeric readings across a dozen attempts. Smaller or faster models produce a recognisable page and stall under the hero gate; if the model in hand is one of those, say so before the direction round and take the code-led path, or expect the run to end at the hero with its readings unmet. The comp-led path is a frontier-tier job: it asks the builder to hold a measured layout, place plates at their boxes, and act on numeric readings across a dozen attempts. Smaller or faster models produce a recognisable page and stall under the hero gate; if the model in hand is one of those, say so before the direction round and take the code-led path, or expect the run to end at the hero with its readings unmet.
1. **spec.** Measure the comp: `impeccable comp-spec --comp <comp> --grid` writes a coordinate grid over the comp; open it, name every salient region by grid span in a regions file (text and control regions snap to the largest ink mass inside their span, so a headline named B1:E4 measures as the headline and not the column beside it; `snap: false` keeps the span, and an explicit `box` is taken as drawn) (kind `plate` / `image` / `texture` for anything painted: every illustration, photograph, figure, product object, and material texture; `text` / `control` / `chrome` for what code draws; every region carries a `note` saying what the comp shows there, which the plate prompt and the gate messages read), and run `impeccable comp-spec --comp <comp> --regions <file>`. The spec carries each region's box, sampled palette, and medium; `impeccable comp-spec --print` is the build's reference from here on. Type is measured, not guessed: `impeccable font-match --measure <text region>` reads the comp's cap height, width class, and weight off the pixels, and `impeccable font-match --rank <region> --text "..."` takes its candidates from a fingerprint index of the Google Fonts catalog (the nearest faces to the crop's shape) plus any names you pass with `--candidates`, renders them at that cap height with the region's words, and ranks them by fingerprint distance (its `USE` line is the CSS; its proof sheet shows the comp over the top three); with no browser resolvable it records the catalog's nearest face and says the size is estimated, which is still the choice to build on. Do not install a browser to rank, and never write a `chosen` face into the spec by hand: the gate accepts only what font-match wrote. The spec gate refuses to close until the lead text region is measured and ranked. A region note that describes painted material (a diagram, drawing, photograph, texture) under a code kind is refused at the spec: reclassify it as a plate, or reword the note if code really draws it. The script refuses a regions file that leaves comp ink unnamed (callouts, a parts table, a notes block): what is never named can never be missing, so everything the comp shows gets a region. It also refuses a `text` / `control` / `chrome` region larger than a quarter of the comp: that is a column, not an element, and a column scored as one region hides the plates, tables, and notes inside it. Name each element inside it (`container: true` only when it truly is one undivided element). Anything drawn is a plate: an inline SVG past an icon's budget (a diagram, notation, leader lines with arrows, a "quick approximation" of the artwork) is refused at the hero; icon-sized SVG (under 64px, a few paths) is fine, and a chart the page draws from data at runtime is a chart, not an illustration. Callout lines and arrows that annotate a drawing belong to that drawing's plate, with only their labels set as text. A crop of the comp is never a plate (the plates gate refuses a file that is a resample of the comp region: the comp's grain, its neighbours' edges, and its resolution would ship as the artwork); the crop is the reference the plate is generated from. A plate region's box has to hold its whole artwork with a margin: the spec measures the artwork's contact with the box edges and refuses a box that cuts through it (`bleed: true` only when the page really crops it there), because a plate placed with `object-fit: cover` on such a box shows the artwork minus the side the box lost. Anything not in the spec does not exist on the page: no borders, rules, containers, or chrome the comp does not show. Only three concessions exist: fonts (the closest obtainable face), icon glyphs (close enough, exact if the user chose an icon library; this covers the pictogram only, never a control's chrome, so a chevron, an arrow, a dropdown's border and fill, a button's shape are the comp's), and genuine defects in the comp such as spelling errors. 1. **spec.** Measure the comp: `impeccable comp-spec --comp <comp> --grid` writes a coordinate grid over the comp; open it, name every salient region by grid span in a regions file (text and control regions snap to the largest ink mass inside their span, so a headline named B1:E4 measures as the headline and not the column beside it; `snap: false` keeps the span, and an explicit `box` is taken as drawn) (kind `plate` / `image` / `texture` for anything painted: every illustration, photograph, figure, product object, and material texture; `text` / `control` / `chrome` for what code draws; every region carries a `note` saying what the comp shows there, which the plate prompt and the gate messages read), and run `impeccable comp-spec --comp <comp> --regions <file>`. The spec carries each region's box, sampled palette, and medium; `impeccable comp-spec --print` is the build's reference from here on. Type is measured, not guessed: `impeccable font-match --measure <text region>` reads the comp's cap height, width class, and weight off the pixels, and `impeccable font-match --rank <region> --text "..."` takes its candidates from a fingerprint index of the Google Fonts catalog (the nearest faces to the crop's shape) plus any names you pass with `--candidates`, renders them at that cap height with the region's words, and ranks them by fingerprint distance (its `USE` line is the CSS; its proof sheet shows the comp over the top three); with no browser resolvable it records the catalog's nearest face and says the size is estimated, which is still the choice to build on. Do not install a browser to rank, and never write a `chosen` face into the spec by hand: the gate accepts only what font-match wrote. The spec gate refuses to close until the lead text region is measured and ranked. A region note that describes painted material (a diagram, drawing, photograph, texture) under a code kind is refused at the spec: reclassify it as a plate, or reword the note if code really draws it. The script refuses a regions file that leaves comp ink unnamed (callouts, a parts table, a notes block): what is never named can never be missing, so everything the comp shows gets a region. It also refuses a `text` / `control` / `chrome` region larger than a quarter of the comp: that is a column, not an element, and a column scored as one region hides the plates, tables, and notes inside it. Name each element inside it (`container: true` only when it truly is one undivided element). Anything drawn is a plate: an inline SVG past an icon's budget (a diagram, notation, leader lines with arrows, a "quick approximation" of the artwork) is refused at the hero; icon-sized SVG (under 64px, a few paths) is fine, and a chart the page draws from data at runtime is a chart, not an illustration. Callout lines and arrows that annotate a drawing belong to that drawing's plate, with only their labels set as text. A crop of the comp is never a plate (the plates gate refuses a file that is a resample of the comp region: the comp's grain, its neighbours' edges, and its resolution would ship as the artwork); the crop is the reference the plate is generated from. A plate region's box has to hold its whole artwork with a margin: the spec measures the artwork's contact with the box edges and refuses a box that cuts through it (`bleed: true` only when the page really crops it there), because a plate placed with `object-fit: cover` on such a box shows the artwork minus the side the box lost. Anything not in the spec does not exist on the page: no borders, rules, containers, or chrome the comp does not show. Only three concessions exist: fonts (the closest obtainable face), icon glyphs (close enough, exact if the user chose an icon library; this covers the pictogram only, never a control's chrome, so a chevron, an arrow, a dropdown's border and fill, a button's shape are the comp's), and genuine defects in the comp such as spelling errors.
2. **plates.** Every raster region ships as a plate: an illustration, photo, or figure regenerated at asset resolution from its comp crop, UI text removed, at its `plate` path (isolated ink, figures, or objects use native transparent PNG so they sit on the page's own ground; photos and textures stay opaque); a texture (paper, cloth, grain) is a clean patch of the comp region mirror-tiled to size, generated only when no clean patch exists. `impeccable comp-spec --crop <id>` writes the reference; save `impeccable comp-spec --plate-prompt <id> --background transparent` to a prompt file for a cutout, or use `--background opaque` otherwise. Prefer the harness-native image tool with that crop and prompt, then `impeccable embed-prompt <plate> --prompt-file <prompt.txt>`. The API fallback is `impeccable generate-image --ref <crop.png> --prompt-file <prompt.txt> --out <plate.png> --size <WxH> --quality high --background transparent` (use `--background opaque` for full-frame assets); create the output directory first. Verify actual alpha, white foregrounds, fine edges, and clear holes on light and dark grounds; do not chroma-key native output. After the initial assets exist, prepare the isolated code component previews and complete [component-review.md](component-review.md) before further gate-driven repair: the user reviews the whole component kit, including code, before page assembly. This checkpoint keeps the existing gates; it does not require passing them first. After the full page and responsive checks, use the assembled-hero checkpoint before the final response. The plates gate scores the assets against the comp; also inspect placement and scale visually. With parallel subagents, spawn the shipped asset producer (`impeccable-asset-producer`; `impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent") with the spec path and let it produce them all; without subagents, produce them here. A crop of the comp is a reference, never a shipping pixel. The gate checks every plate exists, is at least 1.5x the region's size, and reads as the region. Page code waits for this gate: a page written before its plates exist is a page that draws its material in CSS. A single-file deliverable changes nothing here: the plate is produced the same way and inlined as a data URI. `--force` exists for one case only, the user downgrading the comp's authority in words you quote in `--reason`; the script refuses every other reason. 2. **plates.** Every raster region ships as a plate: an illustration, photo, or figure regenerated at asset resolution from its comp crop, UI text removed, at its `plate` path (isolated ink, figures, or objects use native transparent PNG so they sit on the page's own ground; photos and textures stay opaque); a texture (paper, cloth, grain) is a clean patch of the comp region mirror-tiled to size, generated only when no clean patch exists. `impeccable comp-spec --crop <id>` writes the reference; save `impeccable comp-spec --plate-prompt <id> --background transparent` to a prompt file for a cutout, or use `--background opaque` otherwise. Prefer the harness-native image tool with that crop and prompt, then `impeccable embed-prompt <plate> --prompt-file <prompt.txt>`. The API fallback is `impeccable generate-image --ref <crop.png> --prompt-file <prompt.txt> --out <plate.png> --size <WxH> --quality high --background transparent` (use `--background opaque` for full-frame assets); create the output directory first. Verify actual alpha, white foregrounds, fine edges, and clear holes on light and dark grounds; do not chroma-key native output. The plates gate scores the assets against the comp; also inspect placement and scale visually. With parallel subagents, spawn the shipped asset producer (`impeccable-asset-producer`; `impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent") with the spec path and let it produce them all; without subagents, produce them here. A crop of the comp is a reference, never a shipping pixel. The gate checks every plate exists, is at least 1.5x the region's size, and reads as the region. Page code waits for this gate: a page written before its plates exist is a page that draws its material in CSS. A single-file deliverable changes nothing here: the plate is produced the same way and inlined as a data URI. `--force` exists for one case only, the user downgrading the comp's authority in words you quote in `--reason`; the script refuses every other reason.
3. **hero.** `impeccable build-phase scaffold` first: it writes the measured layout as CSS custom properties (`.impeccable/build/scaffold/layout.css`: `--r-<id>-x/y/w/h` in % of the comp, plus cap height, font-size, family, and weight where measured) and a reference page (`hero-reference.html`) with every region at its box and every plate placed. Bind the numbers to your own semantic structure, an element per region; the reference is a check on positions, never the page, and overlapping boxes are overlapping boxes. Then build only the first viewport, at the comp's own dimensions, the comp's words copied verbatim (the user approved that comp with those words; rewording is a stated decision after the hero passes, never a silent one inside it), every text region sized from its measured cap height and set in its ranked face, plates first: place every plate at its spec box (`object-fit: cover`, an `<img>`, a background image, or an inlined data URI named for it) before any text or control, capture into `.impeccable/review/hero-repro.png`, run `impeccable build-phase record hero` once so you see the plate regions read as match before any text exists, then lay the semantic layer over the plates from the spec's palette and boxes and advance. The gate first refuses while any plate is unreferenced by the source, then runs `impeccable comp-diff`, writes `.impeccable/review/diff/hero/` (side-by-side, heatmap, one paired crop per region, `report.json`), and passes at 72% overall with no hard veto outstanding (a missing region, a contradicted plate or text block, an SVG illustration, a clipped plate, invented ink block at any score); above the bar, the numeric readings become advisories printed with the pass, and the polish pass before responsive is where they get fixed: the gate also reads each text region's cap height, line count, weight, ink colour, and position against the comp, each chrome strip's height off its rule, and the frame for ink where the comp is calm (a kicker, an extra nav item, a divider), and says each miss as a number ("cap height 78px in the build, 103px in the comp"); those numbers are the edit. When it fails, open the region crops it lists, in order, before editing: a region scored `missing` needs its material, `contradicted` needs its structure re-derived from the spec box, `drift` is where size and spacing edits belong; the gate refuses a third attempt that only nudges values on the same region. This is where the run's ambition is won or lost, and a retry here costs minutes where a rebuild verdict at the finish costs the run.
3. **hero.** `impeccable build-phase scaffold` first: it writes the measured layout as CSS custom properties (`.impeccable/build/scaffold/layout.css`: `--r-<id>-x/y/w/h` in % of the comp, plus cap height, font-size, family, and weight where measured) and a reference page (`hero-reference.html`) with every region at its box and every plate placed. Bind the numbers to your own semantic structure, an element per region; the reference is a check on positions, never the page, and overlapping boxes are overlapping boxes. Then build only the first viewport, at the comp's own dimensions, the comp's words copied verbatim (the user approved that comp with those words; rewording is a stated decision after the hero passes, never a silent one inside it), every text region sized from its measured cap height and set in its ranked face, plates first: place every plate at its spec box (`object-fit: cover`, an `<img>`, a background image, or an inlined data URI named for it) before any text or control, capture into `.impeccable/review/hero-repro.png`, run `impeccable build-phase record hero` once so you see the plate regions read as match before any text exists, then lay the semantic layer over the plates from the spec's palette and boxes and advance. The gate first refuses while any plate is unreferenced by the source, then runs `impeccable comp-diff`, writes `.impeccable/review/diff/hero/` (side-by-side, heatmap, one paired crop per region, `report.json`; `raw-report.json` preserves the uninterpreted measurements). The report and crop labels use the gate's verdicts; `gate.reasons` lists the remaining blockers even when a region is called drift. An accepted plate is revalidated if its file, measured region, or comp changes. The gate passes at 72% overall with no hard veto outstanding (a missing region, a contradicted plate or text block, an SVG illustration, a clipped plate, invented ink block at any score); above the bar, the numeric readings become advisories printed with the pass, and the polish pass before responsive is where they get fixed: the gate also reads each text region's cap height, line count, weight, ink colour, and position against the comp, each chrome strip's height off its rule, and the frame for ink where the comp is calm (a kicker, an extra nav item, a divider), and says each miss as a number ("cap height 78px in the build, 103px in the comp"); those numbers are the edit. When it fails, open the region crops it lists, in order, before editing: a region scored `missing` needs its material, `contradicted` needs its structure re-derived from the spec box, `drift` is where size and spacing edits belong; repeated attempts do not clear unresolved blockers. This is where the run's ambition is won or lost, and a retry here costs minutes where a rebuild verdict at the finish costs the run.
4. **sections.** Build the rest of the surface inside the spec's system: the same corner language, line weights, and palette, and nothing the comp never shows. Where the comp does not cover a region, it inherits the recorded system. 4. **sections.** Build the rest of the surface inside the spec's system: the same corner language, line weights, and palette, and nothing the comp never shows. Where the comp does not cover a region, it inherits the recorded system.
5. **motion.** The signature interaction, reveals, and motion, orchestrated once rather than scattered. 5. **motion.** The signature interaction, reveals, and motion, orchestrated once rather than scattered.
6. **responsive.** The other viewports, and the first viewport at common desktop widths (1280 to 1600), not only at the comp's exact size: fluid columns, no fixed-pixel grid that wraps a hundred pixels narrower. Capture `desktop.png` (1440 wide, full page) and `mobile.png` (390 wide) into `.impeccable/review/`; the gate diffs the desktop capture against the comp and refuses a first viewport that only held at the comp's width. A comp'd surface that is mobile-first was comped portrait; the plates were produced for that frame. 6. **responsive.** The other viewports, and the first viewport at common desktop widths (1280 to 1600), not only at the comp's exact size: fluid columns, no fixed-pixel grid that wraps a hundred pixels narrower. Capture `desktop.png` (1440 wide, full page) and `mobile.png` (390 wide) into `.impeccable/review/`; the gate diffs the desktop capture against the comp and refuses a first viewport that only held at the comp's width. A comp'd surface that is mobile-first was comped portrait; the plates were produced for that frame.
@@ -144,5 +143,3 @@ A rebuild and a fix round share one asset rule: a raster either round creates or
Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice.
After the last correction, spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, artifact path, direction contract, PRODUCT.md, [document.md](document.md), and write boundary. Without subagents, load [degraded/documenter.md](degraded/documenter.md) and [document.md](document.md) before writing. Verify the outcome: new worlds and approved system changes require token-bearing DESIGN.md **and** `.impeccable/design.json`, not prose alone. Ordinary extensions compare the finished build against the incumbent system, preserve its files, and report the evidence checked; report pre-existing drift without repairing it unasked. Recheck after later edits. Finish only when review and documentation are complete. After the last correction, spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, artifact path, direction contract, PRODUCT.md, [document.md](document.md), and write boundary. Without subagents, load [degraded/documenter.md](degraded/documenter.md) and [document.md](document.md) before writing. Verify the outcome: new worlds and approved system changes require token-bearing DESIGN.md **and** `.impeccable/design.json`, not prose alone. Ordinary extensions compare the finished build against the incumbent system, preserve its files, and report the evidence checked; report pre-existing drift without repairing it unasked. Recheck after later edits. Finish only when review and documentation are complete.
On a comp-led build, record the final review disposition with `.gemini/skills/impeccable/scripts/impeccable build-phase finish --disposition <ship|fix|rebuild|recapture>` before the final response. A refused `ship` is an unfinished build; report the outstanding phase with the verdict.
@@ -16,7 +16,7 @@ Reason over the signals; there is no score to obey:
- `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique <surface>` is a strong default. - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique <surface>` is a strong default.
- `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog and closes it when stale or cleared). - `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog and closes it when stale or cleared).
- `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them.
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. **`live` and the bundled `impeccable detect` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with either; the browser overlay and the HTML rule engine don't apply to native app code. - `devServer.running` true → `live` is available for in-browser iteration, and `generate` for one-shot variant runs on a named element; if false, don't lead with either. **`live`, `generate`, and the bundled `impeccable detect` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with any of them; the browser overlay and the HTML rule engine don't apply to native app code.
- Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`. - Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`.
**If `scan.targets` is non-empty and `setup.platform` is not `ios`/`android`/`adaptive`, run `.gemini/skills/impeccable/scripts/impeccable detect --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx; it reads HTML/CSS, so skip it for native projects). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it. **If `scan.targets` is non-empty and `setup.platform` is not `ios`/`android`/`adaptive`, run `.gemini/skills/impeccable/scripts/impeccable detect --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx; it reads HTML/CSS, so skip it for native projects). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
+1 -1
View File
@@ -1 +1 @@
0.1.6 0.1.5
@@ -19,6 +19,10 @@
"description": "Interactive live variant mode. Select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via HMR. Requires a running dev server. Use when you want to visually experiment with design alternatives in real time.", "description": "Interactive live variant mode. Select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via HMR. Requires a running dev server. Use when you want to visually experiment with design alternatives in real time.",
"argumentHint": "" "argumentHint": ""
}, },
"generate": {
"description": "Agent-driven live variant generation. Boots live mode, finds the named element on the open page, scrolls the browser to it, and delivers N variants in the requested direction for the user to cycle and accept. Use for requests that name an element and a direction, like 'generate 3 bold variants of the pricing cards', skipping manual element picking.",
"argumentHint": "[count] [direction] variants of [element]"
},
"adapt": { "adapt": {
"description": "Adapt designs to work across different screen sizes, devices, contexts, or platforms. Implements breakpoints, fluid layouts, and touch targets. Use when the user mentions responsive design, mobile layouts, breakpoints, viewport adaptation, or cross-device compatibility.", "description": "Adapt designs to work across different screen sizes, devices, contexts, or platforms. Implements breakpoints, fluid layouts, and touch targets. Use when the user mentions responsive design, mobile layouts, breakpoints, viewport adaptation, or cross-device compatibility.",
"argumentHint": "[target] [context (mobile, tablet, print...)]" "argumentHint": "[target] [context (mobile, tablet, print...)]"
@@ -165,6 +165,14 @@
} }
let parameterGenerationState = 'idle'; let parameterGenerationState = 'idle';
let parameterReadyAnnouncedSession = null; let parameterReadyAnnouncedSession = null;
// 'agent' when the generate verb fired this session's Go (the generate
// lane declares no knobs, so its bar never shows a pending Tune chip);
// null for every Go a user presses.
let sessionOrigin = null;
// The generate lane picks for the agent and never edits copy in the
// browser, so its selection carries no edit-copy badge (set on the
// agent-target pick, cleared with the session; a user's pick never sets it).
let editBadgeSuppressed = false;
let svelteComponentSession = null; let svelteComponentSession = null;
let svelteRuntimePromise = null; let svelteRuntimePromise = null;
let pendingSvelteComponentRetryObserver = null; let pendingSvelteComponentRetryObserver = null;
@@ -983,9 +991,20 @@
} }
} catch { /* cross-origin */ } } catch { /* cross-origin */ }
} }
// The selector a mechanical bake would anchor lasting rules on, and how
// many elements it matches right now: the bake refuses anything but one,
// since its rules would restyle every match, not just this element.
const cssIdent = (s) => /^[A-Za-z_-][\w-]*$/.test(s);
const anchorClasses = [...el.classList].filter(cssIdent);
const anchor = el.id && cssIdent(el.id)
? '#' + el.id
: (anchorClasses.length ? el.tagName.toLowerCase() + '.' + anchorClasses.join('.') : null);
let anchorMatches = null;
if (anchor) { try { anchorMatches = document.querySelectorAll(anchor).length; } catch { anchorMatches = null; } }
return { return {
tagName: el.tagName.toLowerCase(), id: el.id || null, tagName: el.tagName.toLowerCase(), id: el.id || null,
classes: [...el.classList], classes: [...el.classList],
anchor, anchorMatches,
textContent: (el.textContent || '').slice(0, 500), textContent: (el.textContent || '').slice(0, 500),
outerHTML: sanitizedContextOuterHTML(el, 10000), outerHTML: sanitizedContextOuterHTML(el, 10000),
computedStyles: { computedStyles: {
@@ -2037,6 +2056,7 @@
function setLiveState(next) { function setLiveState(next) {
state = next; state = next;
window.__IMPECCABLE_LIVE_STATE__ = next; window.__IMPECCABLE_LIVE_STATE__ = next;
retryDeclinedAgentTargets();
syncPageInteractionCursor(); syncPageInteractionCursor();
// Whether a queued steer is still behind a generation is a function of this // Whether a queued steer is still behind a generation is a function of this
// state, so the hint has to move with it, not only with the 5s poll. // state, so the hint has to move with it, not only with the 5s poll.
@@ -4014,6 +4034,7 @@
function hidePendingApplyDock() { function hidePendingApplyDock() {
pendingApplyInFlight = false; pendingApplyInFlight = false;
retryDeclinedAgentTargets();
clearStoredManualApplyState(); clearStoredManualApplyState();
if (pendingIntroAnimation) { pendingIntroAnimation.cancel(); pendingIntroAnimation = null; } if (pendingIntroAnimation) { pendingIntroAnimation.cancel(); pendingIntroAnimation = null; }
if (pendingDockEl) pendingDockEl.style.display = 'none'; if (pendingDockEl) pendingDockEl.style.display = 'none';
@@ -4047,6 +4068,7 @@
function setPendingApplyLoading(loading, count) { function setPendingApplyLoading(loading, count) {
if (!pendingPillEl || !pendingPillLabelEl || !pendingPillCountEl || !pendingTrashBtn) return; if (!pendingPillEl || !pendingPillLabelEl || !pendingPillCountEl || !pendingTrashBtn) return;
pendingApplyInFlight = loading === true; pendingApplyInFlight = loading === true;
if (!pendingApplyInFlight) retryDeclinedAgentTargets();
const currentCount = count || parseInt(pendingPillEl.dataset.count || '0', 10) || 0; const currentCount = count || parseInt(pendingPillEl.dataset.count || '0', 10) || 0;
if (pendingApplyInFlight) storeManualApplyState(currentCount); if (pendingApplyInFlight) storeManualApplyState(currentCount);
else clearStoredManualApplyState(); else clearStoredManualApplyState();
@@ -4688,6 +4710,7 @@
} }
function renderEditBadge(mode) { function renderEditBadge(mode) {
if (editBadgeSuppressed || sessionOrigin === 'agent') mode = 'hidden';
if (mode === 'hidden' || !editBadgeEl) { if (mode === 'hidden' || !editBadgeEl) {
hideConfigureBarTooltip(); hideConfigureBarTooltip();
if (editBadgeEl) editBadgeEl.style.display = 'none'; if (editBadgeEl) editBadgeEl.style.display = 'none';
@@ -6181,6 +6204,8 @@
resetSessionFileMeta(); resetSessionFileMeta();
currentSessionId = null; currentSessionId = null;
parameterGenerationState = 'idle'; parameterGenerationState = 'idle';
sessionOrigin = null;
editBadgeSuppressed = false;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
expectedVariants = 0; expectedVariants = 0;
arrivedVariants = 0; arrivedVariants = 0;
@@ -7112,6 +7137,398 @@
} }
// //
// ------------------------------------------------------------------
// Agent-initiated targeting (the `generate` command). The agent names an
// element by CSS selector over POST /agent-target; the server pushes an
// `agent_target` SSE message here. The overlay resolves the selector,
// scrolls the element into view, enters the same picked state a user
// click produces, and fires the normal Go pipeline, so everything
// downstream (generate event, variants, cycling, accept) is unchanged.
// The verdict goes back through POST /agent-target-result, which resolves
// the agent's held-open CLI call.
function postAgentTargetResult(targetId, result) {
fetch('http://localhost:' + PORT + '/agent-target-result?token=' + TOKEN, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: TOKEN, targetId, clientId: AGENT_TARGET_CLIENT_ID, ...result }),
}).catch(() => { /* server gone; nothing to report to */ });
}
function describeAgentTargetCandidate(el) {
return {
tag: el.tagName.toLowerCase(),
id: el.id || null,
classes: [...el.classList].filter((c) => !c.startsWith('impeccable-')),
text: (el.textContent || '').trim().slice(0, 80),
};
}
function resolveAgentTargetElement(msg) {
let matched;
try {
matched = [...document.querySelectorAll(msg.selector)];
} catch {
return { error: { ok: false, error: 'invalid_selector', selector: msg.selector } };
}
let candidates = matched.filter((el) => pickable(el));
if (msg.text) {
const needle = String(msg.text).toLowerCase();
candidates = candidates.filter((el) => (el.textContent || '').toLowerCase().includes(needle));
}
if (candidates.length === 0) {
return {
error: {
ok: false,
error: 'no_match',
selector: msg.selector,
matchCount: 0,
// How many nodes the raw selector hit before the pickable/text
// filters: distinguishes a wrong selector from an unpickable match.
rawMatchCount: matched.length,
},
};
}
if (Number.isInteger(msg.index)) {
const el = candidates[msg.index - 1];
if (!el) {
return { error: { ok: false, error: 'index_out_of_range', selector: msg.selector, matchCount: candidates.length } };
}
return { el, matchCount: candidates.length };
}
if (candidates.length > 1) {
return {
error: {
ok: false,
error: 'ambiguous',
selector: msg.selector,
matchCount: candidates.length,
candidates: candidates.slice(0, 8).map(describeAgentTargetCandidate),
},
};
}
return { el: candidates[0], matchCount: 1 };
}
function scrollAgentTargetIntoView(el, done) {
const rect = el.getBoundingClientRect();
if (rect.top >= 0 && rect.bottom <= window.innerHeight) { done(); return; }
let settled = false;
let fallback = null;
const finish = () => {
if (settled) return;
settled = true;
removeEventListener('scrollend', finish, true);
if (fallback) clearTimeout(fallback);
done();
};
// scrollend where supported; a timer covers engines without it and the
// no-movement case (element already at its final resting position).
addEventListener('scrollend', finish, true);
fallback = setTimeout(finish, 1200);
el.scrollIntoView({ block: 'center', behavior: 'smooth' });
}
// One id per page load: the server keys claims and roll-call reports on
// it, and only the tab that holds the lease can renew it.
const AGENT_TARGET_CLIENT_ID = id8();
// The agent target an agent-initiated Go is serving: set by
// actOnAgentTarget around its handleGo call, read once by handleGo.
let agentTargetForGo = null;
// The helper's word on its global bar. The generate lane asks the helper
// to keep it out of the way (`impeccable live --no-live-bar`, or an agent
// target carrying hideLiveBar), and the helper tells every connected tab
// at once (`live_bar`) and every later connection on `connected`, so the
// bar stays hidden in every tab, through reloads, the accept, and the
// bake, until the helper stops and takes the overlay with it. The variant
// controls still show.
let liveBarHiddenByHelper = false;
function applyLiveBarPreference(hidden) {
liveBarHiddenByHelper = hidden === true;
setLiveBarHidden(liveBarHiddenByHelper);
}
// A plain live session must never notice this code: hiding remembers the
// bar's own display value and restoring puts exactly that back, and a
// restore on a bar that is not hidden is a no-op, so the `connected`
// frame every session receives changes nothing unless the lane asked.
function setLiveBarHidden(hidden) {
if (!globalBarEl) return;
if (hidden) {
if (globalBarEl.style.display !== 'none') {
globalBarEl.dataset.liveBarDisplay = globalBarEl.style.display || 'flex';
globalBarEl.style.display = 'none';
}
return;
}
if (globalBarEl.style.display === 'none') {
globalBarEl.style.display = globalBarEl.dataset.liveBarDisplay || 'flex';
}
}
function claimAgentTarget(targetId, report) {
return fetch('http://localhost:' + PORT + '/agent-target-claim?token=' + TOKEN, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: TOKEN, targetId, clientId: AGENT_TARGET_CLIENT_ID, ...report }),
}).then((res) => res.json())
.then((j) => ({ granted: !!j && j.granted === true, pending: !!j && j.pending === true }))
.catch(() => ({ granted: false, pending: false }));
}
// `exceptTargetId` is the target this call is about: a tab acting on it
// is not busy for itself, but it is busy for every other target, or two
// held requests could both be claimed here and the second Go would
// overwrite the session the first one minted.
function agentTargetBusyReason(exceptTargetId) {
if (pendingApplyInFlight) return 'manual_apply_in_flight';
if (state !== 'IDLE' && state !== 'PICKING' && state !== 'CONFIGURING') return 'session_active';
for (const [targetId, status] of agentTargetsSeen) {
if (status === 'acting' && targetId !== exceptTargetId) return 'agent_target_in_flight';
}
return null;
}
// Targets this tab declined as busy. A busy report is only this tab's word
// at that moment: the moment it is free again (setLiveState), it claims
// each of these as eligible, and the server drops the stale report, so a
// busy verdict is never built on a tab that has since gone idle. The
// server denies claims for resolved targets, so retries are harmless.
const busyDeclinedTargets = new Map();
function declineAgentTargetBusy(msg, busy) {
busyDeclinedTargets.set(msg.targetId, msg);
noteAgentTarget(msg.targetId, 'declined');
claimAgentTarget(msg.targetId, { eligible: false, state, reason: busy });
}
// A torn-down overlay, or one whose helper connection is gone, cannot
// serve a target and must not even claim one: it would hold the lease for
// a request it will never act on.
function agentTargetOverlayGone() {
return !evtSource;
}
// A denied claimant retries at this cadence, a little over the lease, so
// the first retry after a dead holder's lease lapses is granted.
const AGENT_TARGET_RESCUE_RETRY_MS = 3500;
// Claim the lease and act as the holder. A denied claim means another tab
// holds the lease. That holder can die before posting its result (reload,
// crash, even after renewing), and its lease lapses after ~3s, so this tab
// keeps retrying for as long as the server still holds the request: the
// answer's `pending` is the server's word that the request is alive, and
// it turns false the moment the request resolved or timed out, so no tab
// retries a request nobody awaits. A tab that turned busy meanwhile joins
// the roll call instead of taking a lease it cannot use. The first claim
// and the busy-to-idle re-claim share this.
function claimAndActOnAgentTarget(msg) {
if (agentTargetOverlayGone()) return;
const busy = agentTargetBusyReason(msg.targetId);
if (busy) { declineAgentTargetBusy(msg, busy); return; }
if (declineAgentTargetUnresolvable(msg)) return;
claimAgentTarget(msg.targetId, { eligible: true }).then((claim) => {
if (claim.granted) { noteAgentTarget(msg.targetId, 'acting'); actOnAgentTarget(msg); return; }
noteAgentTarget(msg.targetId, 'denied');
if (!claim.pending) return;
setTimeout(() => claimAndActOnAgentTarget(msg), AGENT_TARGET_RESCUE_RETRY_MS);
});
}
function retryDeclinedAgentTargets() {
if (busyDeclinedTargets.size === 0 || agentTargetBusyReason()) return;
for (const [targetId, msg] of busyDeclinedTargets) {
busyDeclinedTargets.delete(targetId);
claimAndActOnAgentTarget(msg);
}
}
// This page's participation in each target it heard: 'acting' once a
// claim was granted, 'done' once it replied (or stood down from a lapsed
// lease), else the word it last gave. The server replays pending targets
// to every connection that opens. After a reconnect that overlapped the
// old connection the server still holds this page's word; after one that
// did not, it dropped the word on the close, so a replayed target is
// handled again: a busy or unresolvable page re-declines (idempotent), an
// idle page claims.
const agentTargetsSeen = new Map();
function noteAgentTarget(targetId, status) {
agentTargetsSeen.set(targetId, status);
if (agentTargetsSeen.size > 100) agentTargetsSeen.delete(agentTargetsSeen.keys().next().value);
}
// A target this page took a lease on is off-limits for a replay: while
// acting (a second claim or Go), and once done, because its result may
// still be on the wire and this tab is GENERATING by then, so handling
// the replay would decline busy, hand the lease back mid-resolution, and
// let another tab fire a second Go.
function agentTargetTaken(targetId) {
const status = agentTargetsSeen.get(targetId);
return status === 'acting' || status === 'done';
}
// Only a page that can resolve the target claims it. A tab whose page
// lacks the element declines with its resolution verdict instead, so a
// first-wins claim never lets the wrong page answer for a target that
// another page has. The server prefers a busy report (a tab that could
// serve later) over these, and returns the resolution verdict only when
// no connected page can serve.
//
// An element can be momentarily absent (a route still rendering, an HMR
// commit mid-swap), so a failed resolution is not this page's final word:
// it is re-checked a few times over about two seconds, claiming the
// moment the element mounts, and only the last miss is reported. The
// server's timeout still bounds the whole exchange.
// The page reports the miss at once (so the other overlays' words can
// complete the roll call) and keeps re-checking at this cadence for as
// long as the server says the request is pending: the server holds an
// all-no_match roll call open for a short grace precisely so a late mount
// can still be claimed, drops the stale report on an eligible claim, and
// ends the watch by answering pending:false once the request resolved or
// timed out.
const AGENT_TARGET_RESOLVE_WATCH_MS = 500;
function declineAgentTargetUnresolvable(msg) {
const probe = resolveAgentTargetElement(msg);
if (!probe.error) return false;
reportAgentTargetUnresolvable(msg, probe.error);
return true;
}
function reportAgentTargetUnresolvable(msg, error) {
noteAgentTarget(msg.targetId, 'declined');
claimAgentTarget(msg.targetId, { eligible: false, state, reason: 'no_match', result: error }).then((answer) => {
if (!answer.pending) return;
setTimeout(() => watchAgentTargetResolution(msg, error), AGENT_TARGET_RESOLVE_WATCH_MS);
});
}
function watchAgentTargetResolution(msg, lastError) {
if (agentTargetOverlayGone() || agentTargetTaken(msg.targetId)) return;
const busy = agentTargetBusyReason(msg.targetId);
if (busy) { declineAgentTargetBusy(msg, busy); return; }
const probe = resolveAgentTargetElement(msg);
if (!probe.error) { claimAndActOnAgentTarget(msg); return; }
// Still unresolvable: re-report (idempotent); the answer says whether
// the server is still holding the request open.
reportAgentTargetUnresolvable(msg, probe.error || lastError);
}
function handleAgentTarget(msg) {
if (!msg || typeof msg.targetId !== 'string') return;
if (agentTargetTaken(msg.targetId)) return;
noteAgentTarget(msg.targetId, 'heard');
const busy = agentTargetBusyReason(msg.targetId);
if (busy) {
// Roll call: a busy tab reports itself and never acts. The server
// answers `busy` the moment every connected overlay has reported, so
// an idle tab elsewhere is never raced by a timer.
declineAgentTargetBusy(msg, busy);
return;
}
if (declineAgentTargetUnresolvable(msg)) return;
// Eligible tabs race for the server's lease and only the holder acts. A
// hidden tab yields a short head start so a visible one wins when both
// exist, and still serves the request on its own: the user finds the
// selection waiting when they return to it.
setTimeout(() => claimAndActOnAgentTarget(msg), document.hidden ? 150 : 0);
}
function actOnAgentTarget(msg) {
if (agentTargetOverlayGone()) return;
// Every exit ends this tab's acting state, so a later target is not
// refused for a Go that already happened or never will.
const reply = (result) => { noteAgentTarget(msg.targetId, 'done'); postAgentTargetResult(msg.targetId, result); };
const busy = agentTargetBusyReason(msg.targetId);
if (busy) {
// Turned busy between claim and act: report it, which also hands the
// lease back so the roll call can complete or a rescuer can claim.
declineAgentTargetBusy(msg, busy);
return;
}
const resolved = resolveAgentTargetElement(msg);
if (resolved.error) {
// The element went away between claim and act. A result would end the
// request for every tab; a decline hands the lease back so another
// page or a remount can still serve it.
reportAgentTargetUnresolvable(msg, resolved.error);
return;
}
const el = resolved.el;
if (msg.dryRun) {
reply({
ok: true,
dryRun: true,
matchCount: resolved.matchCount,
element: describeAgentTargetCandidate(el),
});
return;
}
scrollAgentTargetIntoView(el, () => {
// Torn down during the scroll settle: do not renew. The lease lapses
// for a rescuer instead of Go minting a session on a dismantled
// overlay.
if (agentTargetOverlayGone()) return;
// Renew the lease right before the irreversible part: a tab whose
// lease lapsed while it scrolled (a rescuer took over) stops here, so
// one request never gets two Go presses.
claimAgentTarget(msg.targetId, { eligible: true }).then((renewal) => {
if (!renewal.granted) { noteAgentTarget(msg.targetId, 'done'); return; }
// An insert placement left mid-configure gives way, exactly as a
// click outside it does in handleClick.
if (state === 'CONFIGURING' && configureKind === 'insert') cancelInsertConfigure();
// Mirror of the user-click pick entry in handleClick, minus the
// pick-mode gate (the agent's intent replaces the toggle); the entry
// goes through beginNewLiveConfiguration like every other pick so
// deferred recovery sees a fresh interaction revision.
selectedElement = el;
beginNewLiveConfiguration();
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
showBar('configure');
editBadgeSuppressed = true;
renderEditBadge('hidden');
startScrollTracking();
maybePrefetchPage();
maybeWarnConditionalAncestor(selectedElement);
// Preset what the agent asked for, then fire the same Go a user press
// fires. handleGo reads exactly these inputs.
selectedAction = msg.action;
selectedCount = msg.count;
// updateBarContent rebuilds the configure row and replaces the input
// element, so the prompt must be written into the input it creates,
// never before (the action-chip click handler does the same dance).
updateBarContent('configure');
const input = uiGetById(PREFIX + '-input');
if (input) input.value = msg.prompt || '';
// The target rides on the generate event too: the helper resolves
// the request from whichever lands first, so a page that dies
// between Go and its result cannot leave the request pending for a
// second Go elsewhere.
const candidate = describeAgentTargetCandidate(el);
agentTargetForGo = { targetId: msg.targetId, matchCount: resolved.matchCount, action: msg.action, count: msg.count, element: candidate };
handleGo();
agentTargetForGo = null;
if (state === 'GENERATING' && currentSessionId) {
reply({
ok: true,
matchCount: resolved.matchCount,
sessionId: currentSessionId,
action: msg.action,
count: msg.count,
element: candidate,
});
} else {
reply({ ok: false, error: 'go_failed', state });
}
});
});
}
// SSE (server→browser) + fetch POST (browser→server) // SSE (server→browser) + fetch POST (browser→server)
// Zero-dependency replacement for WebSocket. // Zero-dependency replacement for WebSocket.
// //
@@ -7121,7 +7538,7 @@
const SSE_MAX_RETRIES = 20; // generous: heartbeats keep the connection alive, so retries mean real trouble const SSE_MAX_RETRIES = 20; // generous: heartbeats keep the connection alive, so retries mean real trouble
function connectSSE() { function connectSSE() {
evtSource = new EventSource('http://localhost:' + PORT + '/events?token=' + TOKEN); evtSource = new EventSource('http://localhost:' + PORT + '/events?token=' + TOKEN + '&clientId=' + AGENT_TARGET_CLIENT_ID);
evtSource.onopen = () => { evtSource.onopen = () => {
sseRetries = 0; // reset on successful (re)connect sseRetries = 0; // reset on successful (re)connect
@@ -7132,8 +7549,11 @@
let msg; try { msg = JSON.parse(e.data); } catch { return; } let msg; try { msg = JSON.parse(e.data); } catch { return; }
switch (msg.type) { switch (msg.type) {
case 'connected': case 'connected':
applyLiveBarPreference(msg.hideLiveBar === true);
hasProjectContext = !!msg.hasProjectContext; hasProjectContext = !!msg.hasProjectContext;
if (!hasProjectContext) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000); // The generate lane runs without PRODUCT.md by design and never
// sends the user to init, so its quiet chrome skips this notice.
if (!hasProjectContext && !liveBarHiddenByHelper) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000);
console.log('[impeccable] Live mode connected.'); console.log('[impeccable] Live mode connected.');
syncAgentPollingUi(!!msg.agentPolling); syncAgentPollingUi(!!msg.agentPolling);
startAgentStatusPoll(); startAgentStatusPoll();
@@ -7143,9 +7563,15 @@
syncPageInteractionCursor(); syncPageInteractionCursor();
syncPageChatFocus('sse-connected'); syncPageChatFocus('sse-connected');
break; break;
case 'live_bar':
applyLiveBarPreference(msg.hidden === true);
break;
case 'agent_polling': case 'agent_polling':
syncAgentPollingUi(!!msg.connected); syncAgentPollingUi(!!msg.connected);
break; break;
case 'agent_target':
handleAgentTarget(msg);
break;
case 'agent_phase': case 'agent_phase':
if (msg.id === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) { if (msg.id === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) {
// Advance the visible phase monotonically. A behind/resumed // Advance the visible phase monotonically. A behind/resumed
@@ -7208,6 +7634,11 @@
disableInlineEdit(); disableInlineEdit();
refreshParamsPanel(); refreshParamsPanel();
} }
// The done reply is the agent's last word on this generation:
// with every variant mounted and no knobs declared, the Tune
// chip must stop spinning. A reload between the mount and this
// reply restored the pending state from the cache.
completeParameterGenerationIfReady();
break; break;
} }
// Source fallback when HMR did not land variants in this tab. // Source fallback when HMR did not land variants in this tab.
@@ -7371,6 +7802,15 @@
}).then(async res => { }).then(async res => {
if (res.ok) return res; if (res.ok) return res;
const body = await res.json().catch(() => ({})); const body = await res.json().catch(() => ({}));
// The helper refused to open a session for an agent target it has
// already answered (another page served it after this page's lease
// lapsed mid-capture, or the request timed out): drop the local
// session and hand the surface back.
if (body.error === 'agent_target_already_served' && msg.type === 'generate'
&& msg.id && msg.id === currentSessionId) {
abandonSupersededGo(msg.id);
return null;
}
// The server refused to journal progress for a session it has never // The server refused to journal progress for a session it has never
// seen: this browser is carrying state from another project or a // seen: this browser is carrying state from another project or a
// wiped store (two apps sharing a localhost port). Continuing to // wiped store (two apps sharing a localhost port). Continuing to
@@ -7392,6 +7832,14 @@
return sessionCreationGate.then(doSend); return sessionCreationGate.then(doSend);
} }
function abandonSupersededGo(sessionId) {
if (sessionId !== currentSessionId) return;
console.warn('[impeccable] The helper already answered this agent target; clearing session ' + sessionId + '.');
markSessionHandled();
cleanup({ instantChrome: true });
showToast('The helper already answered this request, so this session was cleared. Pick an element to start fresh.', 6000);
}
let abandonedForeignSessionId = null; let abandonedForeignSessionId = null;
function abandonForeignSession(sessionId) { function abandonForeignSession(sessionId) {
if (abandonedForeignSessionId === sessionId || sessionId !== currentSessionId) return; if (abandonedForeignSessionId === sessionId || sessionId !== currentSessionId) return;
@@ -7796,6 +8244,7 @@
visibleVariant = 0; visibleVariant = 0;
generationPhase = 'queued'; generationPhase = 'queued';
parameterGenerationState = 'pending'; parameterGenerationState = 'pending';
sessionOrigin = agentTargetForGo ? 'agent' : null;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
resetSessionFileMeta(); resetSessionFileMeta();
@@ -7821,6 +8270,24 @@
}; };
if (snapshot.comments.length > 0) basePayload.comments = snapshot.comments; if (snapshot.comments.length > 0) basePayload.comments = snapshot.comments;
if (snapshot.strokes.length > 0) basePayload.strokes = snapshot.strokes; if (snapshot.strokes.length > 0) basePayload.strokes = snapshot.strokes;
if (agentTargetForGo) {
// An agent-initiated Go names the target it serves (see
// actOnAgentTarget): the helper resolves that request from this event
// as well as from the overlay's own result post.
basePayload.agentTarget = {
targetId: agentTargetForGo.targetId,
clientId: AGENT_TARGET_CLIENT_ID,
result: {
ok: true,
matchCount: agentTargetForGo.matchCount,
sessionId: currentSessionId,
action: agentTargetForGo.action,
count: agentTargetForGo.count,
element: agentTargetForGo.element,
},
};
agentTargetForGo = null;
}
// Hide the interactive overlay so it doesn't linger during generation. // Hide the interactive overlay so it doesn't linger during generation.
hideAnnotOverlay(); hideAnnotOverlay();
@@ -7881,6 +8348,7 @@
visibleVariant = 0; visibleVariant = 0;
generationPhase = 'queued'; generationPhase = 'queued';
parameterGenerationState = 'pending'; parameterGenerationState = 'pending';
sessionOrigin = agentTargetForGo ? 'agent' : null;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
resetSessionFileMeta(); resetSessionFileMeta();
selectedElement = placeholderElement; selectedElement = placeholderElement;
@@ -8927,6 +9395,8 @@ void main() {
pagePickSkipClick = false; pagePickSkipClick = false;
currentSessionId = null; currentSessionId = null;
parameterGenerationState = 'idle'; parameterGenerationState = 'idle';
sessionOrigin = null;
editBadgeSuppressed = false;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
selectedAction = 'impeccable'; selectedAction = 'impeccable';
pendingAcceptedSession = null; pendingAcceptedSession = null;
@@ -9018,6 +9488,7 @@ void main() {
paramsCurrentValues = { ...saved.paramValues }; paramsCurrentValues = { ...saved.paramValues };
} }
if (saved.parameterState) parameterGenerationState = saved.parameterState; if (saved.parameterState) parameterGenerationState = saved.parameterState;
sessionOrigin = saved.origin === 'agent' ? 'agent' : null;
if (saved.generationPhase) generationPhase = saved.generationPhase; if (saved.generationPhase) generationPhase = saved.generationPhase;
} }
@@ -9105,7 +9576,12 @@ void main() {
} }
function restoreSessionWithoutWrapper(reason, activeSessions) { function restoreSessionWithoutWrapper(reason, activeSessions) {
const cached = loadSession(); // The session cache is per origin, so a tab on another page of the same
// app sees this page's session too. Only the page that saved it may
// resume it: the server-adoption branch below already applies the same
// check, and a tab on another page has nothing to render for it.
const cachedRaw = loadSession();
const cached = cachedRaw?.id && !pageMatchesCurrent(cachedRaw.pageUrl) ? null : cachedRaw;
// localStorage is a cache, not a gate. A cleared tab, a second browser // localStorage is a cache, not a gate. A cleared tab, a second browser
// profile, or a teardown that dropped local state all leave the durable // profile, or a teardown that dropped local state all leave the durable
// server session as the only record of work in progress; adopt it instead // server session as the only record of work in progress; adopt it instead
@@ -9218,6 +9694,7 @@ void main() {
pageUrl: location.pathname, pageUrl: location.pathname,
paramValues: { ...paramsCurrentValues }, paramValues: { ...paramsCurrentValues },
parameterState: parameterGenerationState, parameterState: parameterGenerationState,
origin: sessionOrigin || undefined,
insertPlaceholder: insertPlaceholderSnapshot || undefined, insertPlaceholder: insertPlaceholderSnapshot || undefined,
pickedAnchor: pickedAnchorSnapshot || undefined, pickedAnchor: pickedAnchorSnapshot || undefined,
pickedAnchorViewportTop: Number.isFinite(pickedAnchorViewportTop) ? pickedAnchorViewportTop : undefined, pickedAnchorViewportTop: Number.isFinite(pickedAnchorViewportTop) ? pickedAnchorViewportTop : undefined,
@@ -9343,6 +9820,8 @@ void main() {
pagePickSkipClick = false; pagePickSkipClick = false;
currentSessionId = null; currentSessionId = null;
parameterGenerationState = 'idle'; parameterGenerationState = 'idle';
sessionOrigin = null;
editBadgeSuppressed = false;
parameterReadyAnnouncedSession = null; parameterReadyAnnouncedSession = null;
selectedAction = 'impeccable'; selectedAction = 'impeccable';
renderEditBadge('hidden'); renderEditBadge('hidden');
@@ -9601,6 +10080,14 @@ void main() {
const resumedState = arrivedVariants > 0 ? 'CYCLING' : 'GENERATING'; const resumedState = arrivedVariants > 0 ? 'CYCLING' : 'GENERATING';
// A reload between the variants mounting and the agent's done reply
// restores a pending Tune state from the cache; the helper knows whether
// that generation already finished.
if (arrivedVariants >= expectedVariants && expectedVariants > 0
&& (parameterGenerationState === 'pending' || parameterGenerationState === 'loading')) {
settleParameterStateFromHelper(sessionId);
}
// Find the visible variant's content element for highlight positioning. // Find the visible variant's content element for highlight positioning.
const isInsert = wrapper.dataset.impeccableMode === 'insert'; const isInsert = wrapper.dataset.impeccableMode === 'insert';
const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null; const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null;
@@ -11065,6 +11552,21 @@ void main() {
} }
} }
// After a resume the cache may say the Tune knobs are still coming while
// the agent already replied done before the reload. The helper's session
// record settles it; otherwise the done reply on SSE does.
function settleParameterStateFromHelper(sessionId) {
fetch('http://localhost:' + PORT + '/status?token=' + TOKEN, { cache: 'no-store' })
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
if (!data || sessionId !== currentSessionId) return;
const session = (data.activeSessions || []).find((s) => s && s.id === sessionId);
if (!session) return;
if (session.generationCompletedAt || session.generationPhase === 'completed') completeParameterGenerationIfReady();
})
.catch(() => { /* the done reply on SSE settles it otherwise */ });
}
function fetchAgentPollingStatus() { function fetchAgentPollingStatus() {
fetch('http://localhost:' + PORT + '/status?token=' + TOKEN, { cache: 'no-store' }) fetch('http://localhost:' + PORT + '/status?token=' + TOKEN, { cache: 'no-store' })
.then((res) => (res.ok ? res.json() : null)) .then((res) => (res.ok ? res.json() : null))
@@ -11104,11 +11606,15 @@ void main() {
uiAppendStyle(s); uiAppendStyle(s);
} }
// The generate lane's helper says so in the served script itself, so a
// lane session never draws the bar at all; every other session mounts
// it exactly as before.
const barHiddenFromStart = window.__IMPECCABLE_LIVE_BAR_HIDDEN__ === true;
globalBarEl = el('div', { globalBarEl = el('div', {
position: 'fixed', bottom: '14px', left: '50%', position: 'fixed', bottom: '14px', left: '50%',
transform: 'translateX(-50%) translateY(20px)', transform: 'translateX(-50%) translateY(20px)',
zIndex: Z.bar + 5, zIndex: Z.bar + 5,
display: 'flex', alignItems: 'stretch', display: barHiddenFromStart ? 'none' : 'flex', alignItems: 'stretch',
gap: '0', gap: '0',
width: 'max-content', width: 'max-content',
background: P.surface, background: P.surface,
@@ -11124,6 +11630,10 @@ void main() {
}); });
globalBarEl.id = PREFIX + '-global-bar'; globalBarEl.id = PREFIX + '-global-bar';
globalBarEl.dataset.theme = theme; globalBarEl.dataset.theme = theme;
if (barHiddenFromStart) {
liveBarHiddenByHelper = true;
globalBarEl.dataset.liveBarDisplay = 'flex';
}
// Brand mark - kinpaku Impeccable icon (site header / favicon paths). // Brand mark - kinpaku Impeccable icon (site header / favicon paths).
const brand = el('span', { const brand = el('span', {
@@ -11519,6 +12029,9 @@ void main() {
// Listen for detection results AND ready signal // Listen for detection results AND ready signal
window.addEventListener('message', onDetectMessage); window.addEventListener('message', onDetectMessage);
updateGlobalBarState(); updateGlobalBarState();
// The helper may already have said the bar stays hidden (a connect
// that raced the bar build, or a reload mid-lane): re-apply it here.
if (liveBarHiddenByHelper) setLiveBarHidden(true);
} }
function updateGlobalBarState() { function updateGlobalBarState() {
@@ -11715,6 +12228,13 @@ void main() {
/** Full teardown: remove all UI, disconnect SSE, clean up. */ /** Full teardown: remove all UI, disconnect SSE, clean up. */
function teardown() { function teardown() {
// Declined targets die with the overlay: the IDLE transition below must
// not re-claim a lease this page can no longer act on. So does the
// target ledger: an 'acting' entry from a Go that never happened must
// not refuse every target the next connection hears.
busyDeclinedTargets.clear();
agentTargetsSeen.clear();
liveBarHiddenByHelper = false;
stopAgentStatusPoll(); stopAgentStatusPoll();
hideAgentPollTooltip(); hideAgentPollTooltip();
if (agentPollTooltipEl) { if (agentPollTooltipEl) {
@@ -14,10 +14,6 @@ Do not redesign. Preserve the reference's visual role, silhouette, palette, ligh
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 `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; 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; report a card too thin to brief a comp, never pad it 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 (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 keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. 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. 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 `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; 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; report a card too thin to brief a comp, never pad it 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 (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 keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. 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.
## Review handoff
After the initial production batch, return the actual files and any unresolved drift to the parent for the user-facing component review in [component-review.md](../reference/component-review.md). The parent includes rendered code regions alongside these rasters. A parent or automatic visual check is not a substitute for that human checkpoint. Apply requested repairs and preserve unchanged assets; do not self-approve them. This checkpoint does not apply to the Decision Comps job above.
## Input Contract ## Input Contract
Expect the measured spec (`.impeccable/build/spec.json`, written by `impeccable comp-spec` from the approved comp), the approved comp path, and the skill scripts path. Optionally: a subset of region ids to produce, extra prompt notes per region, and format or transparency needs. Everything else you need is in the spec: each raster region's id, kind (plate, image, texture), pixel box, sampled palette, aspect, note, and the plate path it must land on. Expect the measured spec (`.impeccable/build/spec.json`, written by `impeccable comp-spec` from the approved comp), the approved comp path, and the skill scripts path. Optionally: a subset of region ids to produce, extra prompt notes per region, and format or transparency needs. Everything else you need is in the spec: each raster region's id, kind (plate, image, texture), pixel box, sampled palette, aspect, note, and the plate path it must land on.
@@ -17,7 +17,7 @@ Expect: the original request; the confirmed user answers; the artifact path(s);
## Checks, in order ## Checks, in order
0. **Evidence.** Before any other check, verify the required captures exist and every capture is valid. Required: the platform's full viewport set (web: `desktop.png` and `mobile.png`; native: one capture per shipped device class), plus every capture the calling brief names as required, a reported user viewport (`user-<width>.png`) included. Valid: no black or blank regions, content matching what the filename claims (a visit capture showing the About section is invalid), the document top visible where the file claims a full page, dimensions that make sense for the named viewport. A required capture that is absent fails exactly like one that is malformed: a viewport nobody captured is a viewport nobody inspected, and it cannot ship. When any capture fails, the whole review changes shape: return `disposition: recapture` as the first line, then one section, `recapture`, listing each missing or invalid file and what a valid capture of it shows, and stop. Never build a matrix on malformed evidence; a verdict derived from a broken capture launders the breakage into an approval, and the parent owes you a full re-review on valid captures, not a scoring round. 0. **Evidence.** Before any other check, verify the required captures exist and every capture is valid. Required: the platform's full viewport set (web: `desktop.png` and `mobile.png`; native: one capture per shipped device class), plus every capture the calling brief names as required, a reported user viewport (`user-<width>.png`) included. Valid: no black or blank regions, content matching what the filename claims (a visit capture showing the About section is invalid), the document top visible where the file claims a full page, dimensions that make sense for the named viewport. A required capture that is absent fails exactly like one that is malformed: a viewport nobody captured is a viewport nobody inspected, and it cannot ship. When any capture fails, the whole review changes shape: return `disposition: recapture` as the first line, then one section, `recapture`, listing each missing or invalid file and what a valid capture of it shows, and stop. Never build a matrix on malformed evidence; a verdict derived from a broken capture launders the breakage into an approval, and the parent owes you a full re-review on valid captures, not a scoring round.
1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and every phase before `review` is `closed` or explicitly `skipped`; an open or failed phase is a material finding. A comp-led config with no state file, or a state whose `comps` phase is neither `closed` nor `skipped`, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. 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, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all. 1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and its `comps` (or `skipped` when a surface round locked the comp), `spec`, `plates`, and `hero` phases are `closed`; a comp-led config with no state file, or a state whose `comps` phase never closed, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. 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, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all.
2. **Fidelity.** Start from the measurement, then judge what it cannot: read `.impeccable/review/diff/final/report.json` (and hero) first; every region scored `missing` or `contradicted` is a matrix row in that state unless the paired crop under `regions/` shows the score is wrong, and you say why; a region scored `match` still gets your eye for lettering character and material, which the numbers do not measure. Then, 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; 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. Three 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; medium is part of the promise. GROUND: the page field's value and temperature against the comp's, sampled from pixels on both sides when tooling allows rather than judged from memory, and read as the net on-screen result where a texture or tile paints over the base color; a ground warmer or cooler than the comp's is contradicted however faithfully the layout matches, and drift toward the rendition prior (warm cream on light grounds, blue-black slate on dark) is the direction to hunt. With no approved comp, 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 renders) as contradicted on its face; imitation material is the single most reliable mark of machine-made design. GROUND narrows rather than lapses: with no comp to sample, a color OWN-WORLD names is the target and the same warmer-or-cooler judgment applies; when OWN-WORLD names none, there is no GROUND authority, and the review says so in place of a verdict, because a target the reviewer invents turns the check into taste. A critique-reference comp on such a build is provocation, not spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is what the image dared that the build did not, and 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. A fix that requires producing an asset says so explicitly ("produce: <region> 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. 2. **Fidelity.** Start from the measurement, then judge what it cannot: read `.impeccable/review/diff/final/report.json` (and hero) first; every region scored `missing` or `contradicted` is a matrix row in that state unless the paired crop under `regions/` shows the score is wrong, and you say why; a region scored `match` still gets your eye for lettering character and material, which the numbers do not measure. Then, 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; 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. Three 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; medium is part of the promise. GROUND: the page field's value and temperature against the comp's, sampled from pixels on both sides when tooling allows rather than judged from memory, and read as the net on-screen result where a texture or tile paints over the base color; a ground warmer or cooler than the comp's is contradicted however faithfully the layout matches, and drift toward the rendition prior (warm cream on light grounds, blue-black slate on dark) is the direction to hunt. With no approved comp, 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 renders) as contradicted on its face; imitation material is the single most reliable mark of machine-made design. GROUND narrows rather than lapses: with no comp to sample, a color OWN-WORLD names is the target and the same warmer-or-cooler judgment applies; when OWN-WORLD names none, there is no GROUND authority, and the review says so in place of a verdict, because a target the reviewer invents turns the check into taste. A critique-reference comp on such a build is provocation, not spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is what the image dared that the build did not, and 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. A fix that requires producing an asset says so explicitly ("produce: <region> 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. 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, 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. 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, 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.
+4 -3
View File
@@ -1,9 +1,9 @@
--- ---
name: impeccable name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks. description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.4.0 version: 4.3.1
user-invocable: true user-invocable: true
argument-hint: "[shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]" argument-hint: "[shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live|generate] [target]"
license: Apache 2.0 license: Apache 2.0
--- ---
@@ -65,7 +65,8 @@ Choose the mode from the requested surface, not the product, and persist it only
| `clarify [target]` | Fix | Improve UX copy, labels, and error messages | [reference/clarify.md](reference/clarify.md) | | `clarify [target]` | Fix | Improve UX copy, labels, and error messages | [reference/clarify.md](reference/clarify.md) |
| `adapt [target]` | Fix | Adapt for different devices and screen sizes | [reference/adapt.md](reference/adapt.md) · native: [reference/adapt.native.md](reference/adapt.native.md) | | `adapt [target]` | Fix | Adapt for different devices and screen sizes | [reference/adapt.md](reference/adapt.md) · native: [reference/adapt.native.md](reference/adapt.native.md) |
| `optimize [target]` | Fix | Diagnose and fix UI performance | [reference/optimize.md](reference/optimize.md) | | `optimize [target]` | Fix | Diagnose and fix UI performance | [reference/optimize.md](reference/optimize.md) |
| `live` | Iterate | Visual variant mode: pick elements in the browser, generate alternatives | [reference/live.md](reference/live.md) | | `live` | Iterate | Visual variant mode: pick elements in the browser, iterate on alternatives | [reference/live.md](reference/live.md) |
| `generate [n] [action] [element]` | Iterate | Variants, versions, or alternatives of a named element to choose from in the live browser; no manual picking | [reference/generate.md](reference/generate.md) |
Routing: Routing:
@@ -188,6 +188,12 @@ Test thoroughly across contexts:
- **Edge cases**: Very small screens (320px), very large screens (4K) - **Edge cases**: Very small screens (320px), very large screens (4K)
- **Slow connections**: Test on throttled network - **Slow connections**: Test on throttled network
**Custom controls** (sliders, drag surfaces, scrollable control strips): a before/after slider can pass every width check above and still refuse to drag on iOS, so exercise each one in scope in the same batched round as the checks above:
- **Primary gesture**: Tap it and confirm it responds as designed, then drag it with the target input method; the drag must complete, not just start
- **Scroll across it**: A swipe along the page's scroll axis across the control scrolls the page or container without activating it; a drag that starts on the control along its axis moves the control, not the page. Neither failure throws an error, so try both
- **Evidence**: Say what produced the evidence: an emulated viewport, synthesized touch input through a browser tool, which engine ran it (Chromium is not Safari), or a physical device. Screenshots and resized viewports verify layout, never a gesture. Name what stayed untested and move on; unreachable hardware is a reported gap, not a blocker
When the adaptation feels native to each context, hand off to `/impeccable polish` for the final pass. When the adaptation feels native to each context, hand off to `/impeccable polish` for the final pass.
--- ---
+2 -1
View File
@@ -48,11 +48,12 @@ Run comprehensive checks across 5 dimensions. Score each dimension 0-4 using the
**Check for**: **Check for**:
- **Fixed widths**: Hard-coded widths that break on mobile - **Fixed widths**: Hard-coded widths that break on mobile
- **Touch targets**: Interactive elements < 44x44px - **Touch targets**: Interactive elements < 44x44px
- **Broken touch interaction**: Custom sliders, drag surfaces, and scrollable control strips whose primary gesture fails under touch, that swallow page scroll or lose the drag to it, or that stay stuck after an interrupted gesture. Code tells: mouse-only handlers, no `touch-action` on a pointer-event drag surface, drag state that nothing clears on cancel, lost capture, or blur. Exercise the gesture when a browser tool can synthesize touch (a rendered viewport proves layout, not the gesture), then say what produced the evidence (emulated viewport, synthesized touch, which engine, physical device) and what stayed untested
- **Horizontal scroll**: Content overflow on narrow viewports - **Horizontal scroll**: Content overflow on narrow viewports
- **Text scaling**: Layouts that break when text size increases - **Text scaling**: Layouts that break when text size increases
- **Missing breakpoints**: No mobile/tablet variants - **Missing breakpoints**: No mobile/tablet variants
**Score 0-4**: 0=Desktop-only (breaks on mobile), 1=Major issues (some breakpoints, many failures), 2=Partial (works on mobile, rough edges), 3=Good (responsive, minor touch target or overflow issues), 4=Excellent (fluid, all viewports, proper touch targets) **Score 0-4**: 0=Desktop-only (breaks on mobile), 1=Major issues (some breakpoints, many failures), 2=Partial (works on mobile, rough edges), 3=Good (responsive, minor touch target or overflow issues), 4=Excellent (fluid, all viewports, proper touch targets, gestures work under touch)
### 5. Implementation Integrity (CRITICAL) ### 5. Implementation Integrity (CRITICAL)
@@ -1,57 +0,0 @@
# Component review
Use this checkpoint on comp-led builds after producing the initial component kit and before composing the page. The approved comp is the reference. The user reviews the actual produced components, including code; a list of planned assets or screenshots supplied by the builder is not a review of what will ship.
## Prepare the component kit
Keep the measured spec's region IDs. Include every visible region: produced raster assets and working HTML/CSS/SVG for text, controls, patterns, decoration and layout elements. A region rendered in code needs an actual review document, not a promise to implement it later. Use semantic HTML for content and controls. Do not flatten the page or combine unrelated regions to avoid review. Report omitted regions so the user can mark what is missing.
Write `.impeccable/review/components.json` with this manifest format:
```json
{
"schemaVersion": 1,
"id": "components",
"title": "Component review",
"stage": "components",
"comp": {"path": ".impeccable/mocks/comp-2.png", "width": 1536, "height": 1024},
"components": [
{
"id": "illustration",
"name": "Illustration",
"medium": "raster",
"box": {"x": 0.5, "y": 0.2, "w": 0.45, "h": 0.7},
"note": "Produced cutout; positioned over the page ground.",
"preview": {"kind": "image", "path": "assets/illustration.png"},
"dependencies": [".impeccable/build/spec.json"]
},
{
"id": "headline",
"name": "Headline",
"medium": "html",
"box": {"x": 0.05, "y": 0.2, "w": 0.4, "h": 0.25},
"note": "Rendered semantic heading and its typography.",
"preview": {"kind": "page", "path": ".impeccable/review/components/headline.html"},
"dependencies": [".impeccable/build/spec.json", "assets/type.woff2"]
}
]
}
```
The coordinates above only illustrate the schema. Use the approved comp's actual pixel dimensions and each measured region's normalized bounds (`x / width`, `y / height`, `w / width`, `h / height`). A code preview is rendered at the comp viewport and cropped to that component's box, so place its content at those coordinates in the review document. Include every file the document uses in `dependencies`, including linked CSS, fonts and images. The runtime also binds the measured spec for the component stage and checks its inventory. Local paths only. Static PNG, WebP and JPEG previews retain their original bytes and actual transparency; never draw a checkerboard into the asset.
Native capture supports stable HTML/CSS and inline SVG. Supply a static review state for motion and keep the implementation's real inputs. A scripted, canvas or otherwise unsupported component is a blocker to report, not permission to substitute a raster or omit it.
## Present and wait
If the harness exposes `component_review`, call it with `manifest_path` set to `.impeccable/review/components.json`. The host captures the component files, presents this same review interface and returns the user's decisions. A suspended request is waiting for the user; it is not a failed build or an approval.
Otherwise run `.github/skills/impeccable/scripts/impeccable component-review capture --manifest .impeccable/review/components.json`, then start `.github/skills/impeccable/scripts/impeccable component-review serve --session <returned session>` in the background. Open the URL it prints in the available browser and wait for the user. Read the result with `.github/skills/impeccable/scripts/impeccable component-review verify --manifest .impeccable/review/components.json`; pending, needs-work and stale input all refuse approval. Never submit the page or write a receipt on the user's behalf.
The user can approve components, request changes, and mark missing regions. Act on their feedback without replacing it with your own favorable verdict. Keep component IDs stable, update the actual implementation and dependency list, and present another round. The UI carries only approvals whose component inputs have not changed. Do not ask the user to reapprove unchanged work. Continue only when the inventory is confirmed and all components are approved.
## Assemble and review
Build the page from the approved component files. Replacing, simplifying or changing an approved component requires a new component review. Run the existing plates and hero gates; human review does not waive their integrity checks.
After the full page and responsive checks are complete, present a second manifest at `.impeccable/review/hero.json`, with `id` and `stage` set to `hero`. Use one page-preview component covering the assembled first viewport, its real HTML entry, and its complete dependency list. The reference stays the approved comp. Call the same host review tool (or native capture/serve/verify workflow) and obtain the user's approval before the final response. Later edits to the reviewed files require a fresh review. A component-kit approval does not approve their assembled layout.
@@ -13,10 +13,6 @@ Do not redesign. Preserve the reference's visual role, silhouette, palette, ligh
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 `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; 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; report a card too thin to brief a comp, never pad it 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 (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 keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. 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. 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 `comp` path the moment it renders. The parent runs several of you in parallel, one per card, so this card is your entire contract; 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; report a card too thin to brief a comp, never pad it 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 (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 keeps the comparison honest. Real product name and real content only; never invent commercial claims, prices, benchmarks, or dates PRODUCT.md does not carry. Exclusions bind those claims, never a medium the card's own world has not excluded: a subject that lives in photographs keeps its photographs. 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.
## Review handoff
After the initial production batch, return the actual files and any unresolved drift to the parent for the user-facing component review in [component-review.md](../reference/component-review.md). The parent includes rendered code regions alongside these rasters. A parent or automatic visual check is not a substitute for that human checkpoint. Apply requested repairs and preserve unchanged assets; do not self-approve them. This checkpoint does not apply to the Decision Comps job above.
## Input Contract ## Input Contract
Expect the measured spec (`.impeccable/build/spec.json`, written by `impeccable comp-spec` from the approved comp), the approved comp path, and the skill scripts path. Optionally: a subset of region ids to produce, extra prompt notes per region, and format or transparency needs. Everything else you need is in the spec: each raster region's id, kind (plate, image, texture), pixel box, sampled palette, aspect, note, and the plate path it must land on. Expect the measured spec (`.impeccable/build/spec.json`, written by `impeccable comp-spec` from the approved comp), the approved comp path, and the skill scripts path. Optionally: a subset of region ids to produce, extra prompt notes per region, and format or transparency needs. Everything else you need is in the spec: each raster region's id, kind (plate, image, texture), pixel box, sampled palette, aspect, note, and the plate path it must land on.
@@ -16,7 +16,7 @@ Expect: the original request; the confirmed user answers; the artifact path(s);
## Checks, in order ## Checks, in order
0. **Evidence.** Before any other check, verify the required captures exist and every capture is valid. Required: the platform's full viewport set (web: `desktop.png` and `mobile.png`; native: one capture per shipped device class), plus every capture the calling brief names as required, a reported user viewport (`user-<width>.png`) included. Valid: no black or blank regions, content matching what the filename claims (a visit capture showing the About section is invalid), the document top visible where the file claims a full page, dimensions that make sense for the named viewport. A required capture that is absent fails exactly like one that is malformed: a viewport nobody captured is a viewport nobody inspected, and it cannot ship. When any capture fails, the whole review changes shape: return `disposition: recapture` as the first line, then one section, `recapture`, listing each missing or invalid file and what a valid capture of it shows, and stop. Never build a matrix on malformed evidence; a verdict derived from a broken capture launders the breakage into an approval, and the parent owes you a full re-review on valid captures, not a scoring round. 0. **Evidence.** Before any other check, verify the required captures exist and every capture is valid. Required: the platform's full viewport set (web: `desktop.png` and `mobile.png`; native: one capture per shipped device class), plus every capture the calling brief names as required, a reported user viewport (`user-<width>.png`) included. Valid: no black or blank regions, content matching what the filename claims (a visit capture showing the About section is invalid), the document top visible where the file claims a full page, dimensions that make sense for the named viewport. A required capture that is absent fails exactly like one that is malformed: a viewport nobody captured is a viewport nobody inspected, and it cannot ship. When any capture fails, the whole review changes shape: return `disposition: recapture` as the first line, then one section, `recapture`, listing each missing or invalid file and what a valid capture of it shows, and stop. Never build a matrix on malformed evidence; a verdict derived from a broken capture launders the breakage into an approval, and the parent owes you a full re-review on valid captures, not a scoring round.
1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and every phase before `review` is `closed` or explicitly `skipped`; an open or failed phase is a material finding. A comp-led config with no state file, or a state whose `comps` phase is neither `closed` nor `skipped`, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. 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, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all. 1. **Persistence.** PRODUCT.md exists. On a comp-led build, `.impeccable/build/state.json` exists and its `comps` (or `skipped` when a surface round locked the comp), `spec`, `plates`, and `hero` phases are `closed`; a comp-led config with no state file, or a state whose `comps` phase never closed, means the comp round was skipped and the build ran from a world description alone, a material finding that outranks craft; a phase closed with a `forced` record is disclosed as a material finding unless the user downgraded the comp in words the packet quotes; a state file whose `hero.gate.score` sits under 0.72, or a missing state file, means the reproduction ran unproven, a material finding, and `.impeccable/review/hero-repro.png` must exist either way. 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, a material finding. Files under `.impeccable/mocks/decision/` are exempt: they are the direction round's dealt hand, produced before any comp round, and imply no approval whatever the build path; a code-led build has no comp round at all.
2. **Fidelity.** Start from the measurement, then judge what it cannot: read `.impeccable/review/diff/final/report.json` (and hero) first; every region scored `missing` or `contradicted` is a matrix row in that state unless the paired crop under `regions/` shows the score is wrong, and you say why; a region scored `match` still gets your eye for lettering character and material, which the numbers do not measure. Then, 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; 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. Three 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; medium is part of the promise. GROUND: the page field's value and temperature against the comp's, sampled from pixels on both sides when tooling allows rather than judged from memory, and read as the net on-screen result where a texture or tile paints over the base color; a ground warmer or cooler than the comp's is contradicted however faithfully the layout matches, and drift toward the rendition prior (warm cream on light grounds, blue-black slate on dark) is the direction to hunt. With no approved comp, 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 renders) as contradicted on its face; imitation material is the single most reliable mark of machine-made design. GROUND narrows rather than lapses: with no comp to sample, a color OWN-WORLD names is the target and the same warmer-or-cooler judgment applies; when OWN-WORLD names none, there is no GROUND authority, and the review says so in place of a verdict, because a target the reviewer invents turns the check into taste. A critique-reference comp on such a build is provocation, not spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is what the image dared that the build did not, and 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. A fix that requires producing an asset says so explicitly ("produce: <region> 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. 2. **Fidelity.** Start from the measurement, then judge what it cannot: read `.impeccable/review/diff/final/report.json` (and hero) first; every region scored `missing` or `contradicted` is a matrix row in that state unless the paired crop under `regions/` shows the score is wrong, and you say why; a region scored `match` still gets your eye for lettering character and material, which the numbers do not measure. Then, 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; 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. Three 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; medium is part of the promise. GROUND: the page field's value and temperature against the comp's, sampled from pixels on both sides when tooling allows rather than judged from memory, and read as the net on-screen result where a texture or tile paints over the base color; a ground warmer or cooler than the comp's is contradicted however faithfully the layout matches, and drift toward the rendition prior (warm cream on light grounds, blue-black slate on dark) is the direction to hunt. With no approved comp, 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 renders) as contradicted on its face; imitation material is the single most reliable mark of machine-made design. GROUND narrows rather than lapses: with no comp to sample, a color OWN-WORLD names is the target and the same warmer-or-cooler judgment applies; when OWN-WORLD names none, there is no GROUND authority, and the review says so in place of a verdict, because a target the reviewer invents turns the check into taste. A critique-reference comp on such a build is provocation, not spec: no element matrix, no adaptation citations, no asset obligations; its one contribution is what the image dared that the build did not, and 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. A fix that requires producing an asset says so explicitly ("produce: <region> 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. 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, 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. 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, 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.
@@ -0,0 +1,101 @@
> **Additional context needed**: only the target element, when the request does not name one that resolves uniquely on the page.
Generate is the fast lane into live mode: the user names an element, a direction, and a count in one sentence, and within a minute they are cycling through variants in their browser. One command boots the helper, hands the element to the overlay in the page your harness already shows (it scrolls to it, selects it, and fires the same Go a click fires) and returns the generate event; one edit writes the variants; one call replies and waits for the user's choice, which the helper bakes into source itself. This file owns the lane's plumbing; from the event onward the design work is [live.md](live.md)'s, unchanged, so read it in full now if you have not this session.
**Web only.** Live mode's browser overlay has no native equivalent; on `ios` / `android` / `adaptive` projects, decline this command and offer `bolder` or `quieter` on the source instead.
The plumbing is where the lane saves time: one command starts the session around the page your harness already shows, one call replies and waits, and nothing here is a browser you have to babysit. The design work is not where it saves time. Setup runs as for any command (`impeccable context`, this reference, craft-floor.md before the edit), and the variants are planned, written, and accepted exactly the way a live session plans, writes, and accepts them.
Three prohibitions cover the known ways this command goes wrong:
- **Never run init or document, and never ask for PRODUCT.md or DESIGN.md.** When they exist, the start command prints them under `boot` and you use them. When they do not, it says so (`contextMissing`, `contextNote`) and you extract the identity from the event (Step 3). A missing file is never a reason to interview the user inside this command; offer `init` in one line after the session ends.
- **Never hand-write a variants wrapper or invent a session id.** Only the browser mints session ids (8 hex characters, at Go). A missing event is fixed by rerunning Step 2, never with a direct source edit.
- **Do not act on hook findings while live markers are in the file**, and do not restyle variants to appease them; the accept verifies the file once the variant is permanent.
## Step 1: Parse the request
Three parts, all from the user's sentence:
- **A number in the request**: that is the count. **No number**: 3. The protocol caps count at 8.
- **The direction wording** maps onto the live action vocabulary; never invent a new action value:
- **bold, bolder, stronger, punchier**: `bolder`
- **quiet, calmer, softer, toned down**: `quieter`
- **simpler, minimal, stripped**: `distill`
- **refined, tightened, polished**: `polish`
- **font and type words**: `typeset`
- **color words**: `colorize`
- **arrangement and spacing words**: `layout`
- **device and breakpoint words**: `adapt`
- **motion words**: `animate`
- **playful words**: `delight`
- **rule-breaking words**: `overdrive`
- **Wording that carries intent but no vocabulary word** ("make it feel like a bank", "warmer", "more premium"): `impeccable`, with the user's wording passed as the prompt.
- **An action fits AND extra intent rides along** ("bolder, but keep it monochrome"): that action, with the rest as the prompt.
- **The wording names no direction at all** ("better", "improve", "nicer", "different", "fresh", "new", "redesign", "fix", "some options", "ideas", "alternatives", or just "variants" with nothing else): Ask the user directly to clarify what you cannot infer. Ask one question, offering the vocabulary: *"Which direction should the variants take? bolder, quieter, simpler (distill), polished, typography (typeset), color (colorize), layout, motion (animate), playful (delight), or rule-breaking (overdrive)."* Map the answer with this list; an answer that is still open ("surprise me", "you pick") is `impeccable` with the user's original wording as the prompt, and Step 2 starts on that answer.
- **The element description** ("the pricing cards", "the hero heading"): Step 2 resolves it to a selector.
Done when you hold an action from the vocabulary (asked for, when the request named no direction), a count from 1 to 8, and the element description.
## Step 2: Reuse the page, then start
**Reuse** the dev server already running and the tab your harness already shows it in; a second server or a second browser window is the failure this step prevents.
1. **Find the dev server**, cheapest source first, and stop at the first hit: the user's message, a browser tab already on the app (Claude Code: an origin in `tabs_context`), a server your harness started (Claude Code: `preview_list`), a terminal that printed its URL. Its origin is your `--dev-url`. **No hit**: leave `--dev-url` off and run the start command with no wait; the boot probes for a running server and its verdict names the move. `browser_needed` carries the `devUrl` it found: open it as in 2, then rerun with `--dev-url <devUrl> --wait-for-browser 60000`. `no_dev_server` means nothing serves the app: start the dev script the way the verdict says (Claude Code: `preview_start`; Cursor: a background terminal; Codex: an exec you yield from), wait for its URL, then rerun with `--dev-url <url>`.
2. **Open the page that renders the element in your browser, then start.** The route the request names, else the one `--target` serves; `--dev-url` takes only the origin.
- **Cursor** (`browser_navigate`) and **Claude Code** (`navigate`, which opens the Browser pane when it is closed and takes the `tabId` from `tabs_context` when a tab is already on that origin): open the URL, then run the start command with `--dev-url <url> --wait-for-browser 60000`. The boot injects the overlay and the page reloads into it while the command waits. Your browser tool is the only opener on these harnesses; the engine ignores `--open` there.
- **No browser tool** (Codex, others): run the start command with `--open --wait-for-browser 120000`; it opens the system browser, and the longer wait covers the user finding the tab. **`browser_open_failed` back**: tell the user the `url` in one line and rerun with `--wait-for-browser 120000`.
```bash
.github/skills/impeccable/scripts/impeccable live-generate --target src/App.jsx --dev-url http://127.0.0.1:5173/ --selector ".pricing-grid" --action bolder --count 3 --boot --wait-for-browser 60000
```
Run it in the foreground in Cursor and Claude Code (it returns within the wait); on Codex, in an exec you yield from, the way Step 3 runs the poll.
- `--target`: the file that renders the element when the request or the project makes it obvious; skip it otherwise.
- `--dev-url`: the origin from 1; omit it and the boot probes.
- `--selector`: a unique class first, then a landmark tag plus class, an id last (every variant mounts a copy of the element, so an id repeats in the DOM). **The request names a repeated component in plural** ("the pricing cards"): target the container that holds the set, so one scoped stylesheet restyles every instance. One read of the source file that renders the element is allowed when the selector is not obvious; `--dry-run` resolves and reports without starting anything when it is not certain.
- `--boot`: runs the lane's boot (PRODUCT.md and DESIGN.md loaded again for the helper, missing files tolerated, dev URL found, bottom bar hidden for the helper's lifetime) and reuses a helper that is already running. Its result rides along as `boot`.
- Also available: `--prompt`, `--text` (keep only matches whose visible text contains a snippet), `--index` (1-based pick among matches).
Read the output in this order: `boot` (or `boot.contextMissing` with `boot.contextNote`: the page is the source of truth, per the note), then `event`, the generate event for `sessionId`, with the same `_instructions` a user's Go gets. Every verdict carries `_instructions`, and they win over your recollection of this file; the ones whose move is a decision of yours:
- **`ambiguous`**: the candidates are listed; target their common container, or rerun with `--text "<visible text>"` or `--index <n>`.
- **`dev_server_gone`**: the dev server stopped answering while the command waited for the page (on Cursor, a server another chat started dies with that chat). Start it the way the verdict says, then rerun with `--dev-url <url>`.
- **`no_match`**: the tab is on a route that does not render the element (navigate to the right route, rerun), or the selector is wrong (derive a better one from the source, or add `--text`).
- **`config_missing` / `config_invalid`** under `bootError`: follow [live-setup.md](live-setup.md) first, then rerun.
- **`event: null`** with `ok: true`: the event was slower than the wait; run `.github/skills/impeccable/scripts/impeccable live-poll` once to collect it, then continue.
Done when the output shows `ok: true`, a `sessionId`, and an `event`, reached with at most one server started and one tab opened by you.
## Step 3: Generate
The event is a standard `generate` event: the picked element's context, a preflighted scaffold, and `_instructions` naming the action's reference, the planning section, and the exact splice. Handle it exactly per live.md's **Handle generate**, which owns everything from the identity lock to the done reply: read the action's reference and craft-floor.md as it says, plan per section 4 (identity first, then mode, then three different primary axes, then the squint test), declare knobs per section 7, and deliver per section 6 (a complete replacement of the element per variant, the preview CSS plus every variant in one edit at the scaffold's splice). The lane changes nothing about what a variant may be: the moves a live session would make on this element (a promoted tier, a restructured set, a reordered card, a different surface) are open here too. Never screenshot the page; the overlay preview is the review channel until accept.
**Reply and wait in one call**, with the file you wrote:
```bash
.github/skills/impeccable/scripts/impeccable live-poll --reply EVENT_ID done --file src/App.jsx --then-poll
```
This replies done (the browser mounts the variants) and then blocks until the user's choice arrives, so run it the way your harness runs a long wait: **Claude Code** in the foreground with your tool's longest timeout (600000 ms), so you are paused until the choice arrives; **Codex** in a yielded foreground exec; **Cursor** in a background terminal with notify on `"type":"(accept|discard|variant_mount_failed|exit)"`. Never pass a short `--timeout=`. While it runs there is nothing else to do: never sleep and never poll its output on a timer; a harness that backgrounds it wakes you when it returns. `{"type":"timeout"}` means the user has not chosen yet: run `live-poll` again and keep waiting. If the edit fails after the browser flipped to GENERATING, `--reply EVENT_ID error "Short reason"` (without `--then-poll`) so the bar resets.
Then tell the user, in one line, where their variants are: *"Three [bolder] variants are live on [the pricing cards]: cycle with the floating bar's arrows, adjust the Tune knobs, and Accept the keeper."*
Outside the replace path, read the matching live.md section before acting: `scaffold.previewMode: "svelte-component"` (Svelte previews are edited as components, and their accept is mechanical), `mode: "insert"`, `variant_mount_failed`, `steer`, `manual_edit_apply`, and any `fallback: "agent-driven"` wrap error.
## Step 4: Accept and close
The call from Step 3 returns the user's choice. **`discard`**: nothing to do. **`accept`**: `_acceptResult.carbonize: true` is the normal case, and the cleanup is live.md's **Required after accept**, unchanged: move the accepted variant's rules into the stylesheet that already owns the element with real selectors, bake the chosen knob values in, unwrap the element and drop every `data-impeccable-*` attribute, delete the inline `<style>` block and both `impeccable-carbonize` markers, then `.github/skills/impeccable/scripts/impeccable live-complete --id SESSION_ID` and confirm `phase: "completed"`. (`baked: true` appears only when the accept was run with `--bake`; then the helper already made the variant permanent and no `live-complete` is owed.)
Close without being asked, the moment the choice is handled:
```bash
.github/skills/impeccable/scripts/impeccable live-server stop
```
Stopping removes the injected script and reloads the page once: the user sees the accepted design with no overlay chrome, still served by their dev server. **Never kill or restart the dev server**, including one you started in Step 2.
- **The user asks for more variants before you closed**: skip the close, run Step 2 again for the next element (the helper is reused), and close after the last choice.
- **Interrupted or unsure of the state**: `.github/skills/impeccable/scripts/impeccable live-status`, then `live-resume`; the journal under `.impeccable/live/sessions/` is canonical.
Done when the helper is stopped and the dev site still answers with the accepted design.
@@ -205,6 +205,11 @@ t('items', { count }) // Handles complex plural rules
- Optimistic updates with rollback - Optimistic updates with rollback
- Conflict resolution - Conflict resolution
**Interrupted gestures** (custom sliders, drag surfaces, scrollable control strips):
- A second finger or pointer lands mid-drag: the first drag keeps its pointer or ends cleanly, never jumps to the new one
- The browser cancels the gesture to scroll (`pointercancel`), capture is lost (`lostpointercapture`), the pointer is released outside the control, or the window loses focus (`blur`) mid-drag: clear the dragging state and release capture
- After each of these, the next tap or drag works without a reload
**Permission states**: **Permission states**:
- No permission to view - No permission to view
- No permission to edit - No permission to edit
@@ -304,6 +309,7 @@ const throttledScroll = throttle(handleScroll, 100);
- Unit tests for edge cases - Unit tests for edge cases
- Integration tests for error scenarios - Integration tests for error scenarios
- E2E tests for critical paths - E2E tests for critical paths
- A behavioral regression for each confirmed gesture fix, when the project's test runner can drive input
- Visual regression tests - Visual regression tests
- Accessibility tests (axe, WAVE) - Accessibility tests (axe, WAVE)
@@ -330,7 +336,10 @@ Test thoroughly with edge cases:
- **Network issues**: Disable internet, throttle connection - **Network issues**: Disable internet, throttle connection
- **Large datasets**: Test with 1000+ items - **Large datasets**: Test with 1000+ items
- **Concurrent actions**: Click submit 10 times rapidly - **Concurrent actions**: Click submit 10 times rapidly
- **Interrupted gestures**: Add a second finger mid-drag, scroll across the control, release outside it, switch windows mid-drag; then drag again
- **Errors**: Force API errors, test all error states - **Errors**: Force API errors, test all error states
- **Empty**: Remove all data, test empty states - **Empty**: Remove all data, test empty states
For gestures, say what produced the evidence (emulated viewport, synthesized touch, which engine, physical device) and name what stayed untested.
When edge cases are covered, hand off to `/impeccable polish` for the final pass. When edge cases are covered, hand off to `/impeccable polish` for the final pass.
@@ -96,7 +96,7 @@ Build the assigned direction, not a safer interpretation of it. The form supplie
When an approved comp exists, it is a spatial contract, not a mood board: only the user can downgrade its authority, in explicit words. Models systematically believe their HTML, CSS, and SVG recreation of an image succeeded when it did not, so the build runs as a state machine on disk whose gates measure the screen against the comp instead of asking you to remember it. Start it once, and let it tell you what is next: When an approved comp exists, it is a spatial contract, not a mood board: only the user can downgrade its authority, in explicit words. Models systematically believe their HTML, CSS, and SVG recreation of an image succeeded when it did not, so the build runs as a state machine on disk whose gates measure the screen against the comp instead of asking you to remember it. Start it once, and let it tell you what is next:
`.github/skills/impeccable/scripts/impeccable build-phase start --direction <seed key> --kind <assigned|pick|challenger|canon> --artifact <entry file>` right after the direction choice (this is also the choice ping; the roll's output names the exact command), or `start --comp <approved comp> --artifact <entry file>` when a surface round already locked one. `.github/skills/impeccable/scripts/impeccable build-phase start --direction <seed key> --kind <assigned|pick|challenger|canon>` right after the direction choice (this is also the choice ping; the roll's output names the exact command), or `start --comp <approved comp>` when a surface round already locked one.
Then, in order, each closed by `.github/skills/impeccable/scripts/impeccable build-phase advance` (every verb below runs as `.github/skills/impeccable/scripts/impeccable <verb>`; exit 2 means the gate failed and printed why; fix that and advance again; write nothing for a later phase while an earlier gate is open): Then, in order, each closed by `.github/skills/impeccable/scripts/impeccable build-phase advance` (every verb below runs as `.github/skills/impeccable/scripts/impeccable <verb>`; exit 2 means the gate failed and printed why; fix that and advance again; write nothing for a later phase while an earlier gate is open):
@@ -104,9 +104,8 @@ Then, in order, each closed by `.github/skills/impeccable/scripts/impeccable bui
The comp-led path is a frontier-tier job: it asks the builder to hold a measured layout, place plates at their boxes, and act on numeric readings across a dozen attempts. Smaller or faster models produce a recognisable page and stall under the hero gate; if the model in hand is one of those, say so before the direction round and take the code-led path, or expect the run to end at the hero with its readings unmet. The comp-led path is a frontier-tier job: it asks the builder to hold a measured layout, place plates at their boxes, and act on numeric readings across a dozen attempts. Smaller or faster models produce a recognisable page and stall under the hero gate; if the model in hand is one of those, say so before the direction round and take the code-led path, or expect the run to end at the hero with its readings unmet.
1. **spec.** Measure the comp: `impeccable comp-spec --comp <comp> --grid` writes a coordinate grid over the comp; open it, name every salient region by grid span in a regions file (text and control regions snap to the largest ink mass inside their span, so a headline named B1:E4 measures as the headline and not the column beside it; `snap: false` keeps the span, and an explicit `box` is taken as drawn) (kind `plate` / `image` / `texture` for anything painted: every illustration, photograph, figure, product object, and material texture; `text` / `control` / `chrome` for what code draws; every region carries a `note` saying what the comp shows there, which the plate prompt and the gate messages read), and run `impeccable comp-spec --comp <comp> --regions <file>`. The spec carries each region's box, sampled palette, and medium; `impeccable comp-spec --print` is the build's reference from here on. Type is measured, not guessed: `impeccable font-match --measure <text region>` reads the comp's cap height, width class, and weight off the pixels, and `impeccable font-match --rank <region> --text "..."` takes its candidates from a fingerprint index of the Google Fonts catalog (the nearest faces to the crop's shape) plus any names you pass with `--candidates`, renders them at that cap height with the region's words, and ranks them by fingerprint distance (its `USE` line is the CSS; its proof sheet shows the comp over the top three); with no browser resolvable it records the catalog's nearest face and says the size is estimated, which is still the choice to build on. Do not install a browser to rank, and never write a `chosen` face into the spec by hand: the gate accepts only what font-match wrote. The spec gate refuses to close until the lead text region is measured and ranked. A region note that describes painted material (a diagram, drawing, photograph, texture) under a code kind is refused at the spec: reclassify it as a plate, or reword the note if code really draws it. The script refuses a regions file that leaves comp ink unnamed (callouts, a parts table, a notes block): what is never named can never be missing, so everything the comp shows gets a region. It also refuses a `text` / `control` / `chrome` region larger than a quarter of the comp: that is a column, not an element, and a column scored as one region hides the plates, tables, and notes inside it. Name each element inside it (`container: true` only when it truly is one undivided element). Anything drawn is a plate: an inline SVG past an icon's budget (a diagram, notation, leader lines with arrows, a "quick approximation" of the artwork) is refused at the hero; icon-sized SVG (under 64px, a few paths) is fine, and a chart the page draws from data at runtime is a chart, not an illustration. Callout lines and arrows that annotate a drawing belong to that drawing's plate, with only their labels set as text. A crop of the comp is never a plate (the plates gate refuses a file that is a resample of the comp region: the comp's grain, its neighbours' edges, and its resolution would ship as the artwork); the crop is the reference the plate is generated from. A plate region's box has to hold its whole artwork with a margin: the spec measures the artwork's contact with the box edges and refuses a box that cuts through it (`bleed: true` only when the page really crops it there), because a plate placed with `object-fit: cover` on such a box shows the artwork minus the side the box lost. Anything not in the spec does not exist on the page: no borders, rules, containers, or chrome the comp does not show. Only three concessions exist: fonts (the closest obtainable face), icon glyphs (close enough, exact if the user chose an icon library; this covers the pictogram only, never a control's chrome, so a chevron, an arrow, a dropdown's border and fill, a button's shape are the comp's), and genuine defects in the comp such as spelling errors. 1. **spec.** Measure the comp: `impeccable comp-spec --comp <comp> --grid` writes a coordinate grid over the comp; open it, name every salient region by grid span in a regions file (text and control regions snap to the largest ink mass inside their span, so a headline named B1:E4 measures as the headline and not the column beside it; `snap: false` keeps the span, and an explicit `box` is taken as drawn) (kind `plate` / `image` / `texture` for anything painted: every illustration, photograph, figure, product object, and material texture; `text` / `control` / `chrome` for what code draws; every region carries a `note` saying what the comp shows there, which the plate prompt and the gate messages read), and run `impeccable comp-spec --comp <comp> --regions <file>`. The spec carries each region's box, sampled palette, and medium; `impeccable comp-spec --print` is the build's reference from here on. Type is measured, not guessed: `impeccable font-match --measure <text region>` reads the comp's cap height, width class, and weight off the pixels, and `impeccable font-match --rank <region> --text "..."` takes its candidates from a fingerprint index of the Google Fonts catalog (the nearest faces to the crop's shape) plus any names you pass with `--candidates`, renders them at that cap height with the region's words, and ranks them by fingerprint distance (its `USE` line is the CSS; its proof sheet shows the comp over the top three); with no browser resolvable it records the catalog's nearest face and says the size is estimated, which is still the choice to build on. Do not install a browser to rank, and never write a `chosen` face into the spec by hand: the gate accepts only what font-match wrote. The spec gate refuses to close until the lead text region is measured and ranked. A region note that describes painted material (a diagram, drawing, photograph, texture) under a code kind is refused at the spec: reclassify it as a plate, or reword the note if code really draws it. The script refuses a regions file that leaves comp ink unnamed (callouts, a parts table, a notes block): what is never named can never be missing, so everything the comp shows gets a region. It also refuses a `text` / `control` / `chrome` region larger than a quarter of the comp: that is a column, not an element, and a column scored as one region hides the plates, tables, and notes inside it. Name each element inside it (`container: true` only when it truly is one undivided element). Anything drawn is a plate: an inline SVG past an icon's budget (a diagram, notation, leader lines with arrows, a "quick approximation" of the artwork) is refused at the hero; icon-sized SVG (under 64px, a few paths) is fine, and a chart the page draws from data at runtime is a chart, not an illustration. Callout lines and arrows that annotate a drawing belong to that drawing's plate, with only their labels set as text. A crop of the comp is never a plate (the plates gate refuses a file that is a resample of the comp region: the comp's grain, its neighbours' edges, and its resolution would ship as the artwork); the crop is the reference the plate is generated from. A plate region's box has to hold its whole artwork with a margin: the spec measures the artwork's contact with the box edges and refuses a box that cuts through it (`bleed: true` only when the page really crops it there), because a plate placed with `object-fit: cover` on such a box shows the artwork minus the side the box lost. Anything not in the spec does not exist on the page: no borders, rules, containers, or chrome the comp does not show. Only three concessions exist: fonts (the closest obtainable face), icon glyphs (close enough, exact if the user chose an icon library; this covers the pictogram only, never a control's chrome, so a chevron, an arrow, a dropdown's border and fill, a button's shape are the comp's), and genuine defects in the comp such as spelling errors.
2. **plates.** Every raster region ships as a plate: an illustration, photo, or figure regenerated at asset resolution from its comp crop, UI text removed, at its `plate` path (isolated ink, figures, or objects use native transparent PNG so they sit on the page's own ground; photos and textures stay opaque); a texture (paper, cloth, grain) is a clean patch of the comp region mirror-tiled to size, generated only when no clean patch exists. `impeccable comp-spec --crop <id>` writes the reference; save `impeccable comp-spec --plate-prompt <id> --background transparent` to a prompt file for a cutout, or use `--background opaque` otherwise. Prefer the harness-native image tool with that crop and prompt, then `impeccable embed-prompt <plate> --prompt-file <prompt.txt>`. The API fallback is `impeccable generate-image --ref <crop.png> --prompt-file <prompt.txt> --out <plate.png> --size <WxH> --quality high --background transparent` (use `--background opaque` for full-frame assets); create the output directory first. Verify actual alpha, white foregrounds, fine edges, and clear holes on light and dark grounds; do not chroma-key native output. After the initial assets exist, prepare the isolated code component previews and complete [component-review.md](component-review.md) before further gate-driven repair: the user reviews the whole component kit, including code, before page assembly. This checkpoint keeps the existing gates; it does not require passing them first. After the full page and responsive checks, use the assembled-hero checkpoint before the final response. The plates gate scores the assets against the comp; also inspect placement and scale visually. With parallel subagents, spawn the shipped asset producer (`impeccable-asset-producer`; `impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent") with the spec path and let it produce them all; without subagents, produce them here. A crop of the comp is a reference, never a shipping pixel. The gate checks every plate exists, is at least 1.5x the region's size, and reads as the region. Page code waits for this gate: a page written before its plates exist is a page that draws its material in CSS. A single-file deliverable changes nothing here: the plate is produced the same way and inlined as a data URI. `--force` exists for one case only, the user downgrading the comp's authority in words you quote in `--reason`; the script refuses every other reason. 2. **plates.** Every raster region ships as a plate: an illustration, photo, or figure regenerated at asset resolution from its comp crop, UI text removed, at its `plate` path (isolated ink, figures, or objects use native transparent PNG so they sit on the page's own ground; photos and textures stay opaque); a texture (paper, cloth, grain) is a clean patch of the comp region mirror-tiled to size, generated only when no clean patch exists. `impeccable comp-spec --crop <id>` writes the reference; save `impeccable comp-spec --plate-prompt <id> --background transparent` to a prompt file for a cutout, or use `--background opaque` otherwise. Prefer the harness-native image tool with that crop and prompt, then `impeccable embed-prompt <plate> --prompt-file <prompt.txt>`. The API fallback is `impeccable generate-image --ref <crop.png> --prompt-file <prompt.txt> --out <plate.png> --size <WxH> --quality high --background transparent` (use `--background opaque` for full-frame assets); create the output directory first. Verify actual alpha, white foregrounds, fine edges, and clear holes on light and dark grounds; do not chroma-key native output. The plates gate scores the assets against the comp; also inspect placement and scale visually. With parallel subagents, spawn the shipped asset producer (`impeccable-asset-producer`; `impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent") with the spec path and let it produce them all; without subagents, produce them here. A crop of the comp is a reference, never a shipping pixel. The gate checks every plate exists, is at least 1.5x the region's size, and reads as the region. Page code waits for this gate: a page written before its plates exist is a page that draws its material in CSS. A single-file deliverable changes nothing here: the plate is produced the same way and inlined as a data URI. `--force` exists for one case only, the user downgrading the comp's authority in words you quote in `--reason`; the script refuses every other reason.
3. **hero.** `impeccable build-phase scaffold` first: it writes the measured layout as CSS custom properties (`.impeccable/build/scaffold/layout.css`: `--r-<id>-x/y/w/h` in % of the comp, plus cap height, font-size, family, and weight where measured) and a reference page (`hero-reference.html`) with every region at its box and every plate placed. Bind the numbers to your own semantic structure, an element per region; the reference is a check on positions, never the page, and overlapping boxes are overlapping boxes. Then build only the first viewport, at the comp's own dimensions, the comp's words copied verbatim (the user approved that comp with those words; rewording is a stated decision after the hero passes, never a silent one inside it), every text region sized from its measured cap height and set in its ranked face, plates first: place every plate at its spec box (`object-fit: cover`, an `<img>`, a background image, or an inlined data URI named for it) before any text or control, capture into `.impeccable/review/hero-repro.png`, run `impeccable build-phase record hero` once so you see the plate regions read as match before any text exists, then lay the semantic layer over the plates from the spec's palette and boxes and advance. The gate first refuses while any plate is unreferenced by the source, then runs `impeccable comp-diff`, writes `.impeccable/review/diff/hero/` (side-by-side, heatmap, one paired crop per region, `report.json`), and passes at 72% overall with no hard veto outstanding (a missing region, a contradicted plate or text block, an SVG illustration, a clipped plate, invented ink block at any score); above the bar, the numeric readings become advisories printed with the pass, and the polish pass before responsive is where they get fixed: the gate also reads each text region's cap height, line count, weight, ink colour, and position against the comp, each chrome strip's height off its rule, and the frame for ink where the comp is calm (a kicker, an extra nav item, a divider), and says each miss as a number ("cap height 78px in the build, 103px in the comp"); those numbers are the edit. When it fails, open the region crops it lists, in order, before editing: a region scored `missing` needs its material, `contradicted` needs its structure re-derived from the spec box, `drift` is where size and spacing edits belong; the gate refuses a third attempt that only nudges values on the same region. This is where the run's ambition is won or lost, and a retry here costs minutes where a rebuild verdict at the finish costs the run.
3. **hero.** `impeccable build-phase scaffold` first: it writes the measured layout as CSS custom properties (`.impeccable/build/scaffold/layout.css`: `--r-<id>-x/y/w/h` in % of the comp, plus cap height, font-size, family, and weight where measured) and a reference page (`hero-reference.html`) with every region at its box and every plate placed. Bind the numbers to your own semantic structure, an element per region; the reference is a check on positions, never the page, and overlapping boxes are overlapping boxes. Then build only the first viewport, at the comp's own dimensions, the comp's words copied verbatim (the user approved that comp with those words; rewording is a stated decision after the hero passes, never a silent one inside it), every text region sized from its measured cap height and set in its ranked face, plates first: place every plate at its spec box (`object-fit: cover`, an `<img>`, a background image, or an inlined data URI named for it) before any text or control, capture into `.impeccable/review/hero-repro.png`, run `impeccable build-phase record hero` once so you see the plate regions read as match before any text exists, then lay the semantic layer over the plates from the spec's palette and boxes and advance. The gate first refuses while any plate is unreferenced by the source, then runs `impeccable comp-diff`, writes `.impeccable/review/diff/hero/` (side-by-side, heatmap, one paired crop per region, `report.json`; `raw-report.json` preserves the uninterpreted measurements). The report and crop labels use the gate's verdicts; `gate.reasons` lists the remaining blockers even when a region is called drift. An accepted plate is revalidated if its file, measured region, or comp changes. The gate passes at 72% overall with no hard veto outstanding (a missing region, a contradicted plate or text block, an SVG illustration, a clipped plate, invented ink block at any score); above the bar, the numeric readings become advisories printed with the pass, and the polish pass before responsive is where they get fixed: the gate also reads each text region's cap height, line count, weight, ink colour, and position against the comp, each chrome strip's height off its rule, and the frame for ink where the comp is calm (a kicker, an extra nav item, a divider), and says each miss as a number ("cap height 78px in the build, 103px in the comp"); those numbers are the edit. When it fails, open the region crops it lists, in order, before editing: a region scored `missing` needs its material, `contradicted` needs its structure re-derived from the spec box, `drift` is where size and spacing edits belong; repeated attempts do not clear unresolved blockers. This is where the run's ambition is won or lost, and a retry here costs minutes where a rebuild verdict at the finish costs the run.
4. **sections.** Build the rest of the surface inside the spec's system: the same corner language, line weights, and palette, and nothing the comp never shows. Where the comp does not cover a region, it inherits the recorded system. 4. **sections.** Build the rest of the surface inside the spec's system: the same corner language, line weights, and palette, and nothing the comp never shows. Where the comp does not cover a region, it inherits the recorded system.
5. **motion.** The signature interaction, reveals, and motion, orchestrated once rather than scattered. 5. **motion.** The signature interaction, reveals, and motion, orchestrated once rather than scattered.
6. **responsive.** The other viewports, and the first viewport at common desktop widths (1280 to 1600), not only at the comp's exact size: fluid columns, no fixed-pixel grid that wraps a hundred pixels narrower. Capture `desktop.png` (1440 wide, full page) and `mobile.png` (390 wide) into `.impeccable/review/`; the gate diffs the desktop capture against the comp and refuses a first viewport that only held at the comp's width. A comp'd surface that is mobile-first was comped portrait; the plates were produced for that frame. 6. **responsive.** The other viewports, and the first viewport at common desktop widths (1280 to 1600), not only at the comp's exact size: fluid columns, no fixed-pixel grid that wraps a hundred pixels narrower. Capture `desktop.png` (1440 wide, full page) and `mobile.png` (390 wide) into `.impeccable/review/`; the gate diffs the desktop capture against the comp and refuses a first viewport that only held at the comp's width. A comp'd surface that is mobile-first was comped portrait; the plates were produced for that frame.
@@ -144,5 +143,3 @@ A rebuild and a fix round share one asset rule: a raster either round creates or
Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice.
After the last correction, spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, artifact path, direction contract, PRODUCT.md, [document.md](document.md), and write boundary. Without subagents, load [degraded/documenter.md](degraded/documenter.md) and [document.md](document.md) before writing. Verify the outcome: new worlds and approved system changes require token-bearing DESIGN.md **and** `.impeccable/design.json`, not prose alone. Ordinary extensions compare the finished build against the incumbent system, preserve its files, and report the evidence checked; report pre-existing drift without repairing it unasked. Recheck after later edits. Finish only when review and documentation are complete. After the last correction, spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, artifact path, direction contract, PRODUCT.md, [document.md](document.md), and write boundary. Without subagents, load [degraded/documenter.md](degraded/documenter.md) and [document.md](document.md) before writing. Verify the outcome: new worlds and approved system changes require token-bearing DESIGN.md **and** `.impeccable/design.json`, not prose alone. Ordinary extensions compare the finished build against the incumbent system, preserve its files, and report the evidence checked; report pre-existing drift without repairing it unasked. Recheck after later edits. Finish only when review and documentation are complete.
On a comp-led build, record the final review disposition with `.github/skills/impeccable/scripts/impeccable build-phase finish --disposition <ship|fix|rebuild|recapture>` before the final response. A refused `ship` is an unfinished build; report the outstanding phase with the verdict.

Some files were not shown because too many files have changed in this diff Show More