- Drop the black rule above the support list (it read as a hard hat
over the column).
- Drop the phase-caption strip below the frame (the demo already
carries its own state visually; the narrated loop underneath was
noise).
- Rewrite the four support cells with stronger POV:
- Start: the actual commands, /impeccable live and stop.
- Your agent, your code: emphasises that it's the same AI you've
been working with, reading your real project context, generating
real edits.
- Accept = write: canvas-like iteration, production output. The
point almost no other AI design tool can claim.
- Skill-native: no external tools or IDE extensions, works across
every harness the skill targets.
- Drop CSP-patch bullet (edge case, not worth homepage real estate).
- Drop the "Accepts into ... via HMR" framing — accepts work because
of the skill loop, not because of HMR per se.
Four responses to feedback:
1. Language: fold "Pick any command to see it in action / Full command
reference →" into the section-lead. The whole commands-header row
is gone; the fisheye panel now sits much closer to the lead copy.
2. Antidote: full two-column editorial split. Left column carries the
lead + a three-row stats block (25 / 6 / 3) and the "Browse full
catalog" CTA as a squared editorial link. Right column is the
Gallery of Shame card stack. No more empty right-of-gallery void.
3. The Case: tab labels switch from Cormorant italic serif to body
sans. Tab numbers stay mono. Less aesthetic noise, cleaner reading.
4. Live Mode:
- Layout split: the frame now sits in the left column with its
caption; the support list ("Works on / Accepts into / CSP /
Overlay") moves to a narrow right column, rule-delimited, with
magenta-kicker-above-ink style per cell. Under the support list
on desktop, stacks on mobile.
- Skeleton page: nav bar (logo + 3 links + CTA) plus heading +
body lines drawn in at 30% opacity behind the target card, so
the iteration reads as "I clicked an element on an actual page."
- Variant cards rewritten with distinct design language per card:
V1 editorial column (rule-top, italic display, text-link button),
V2 brutalist ink ticket (ink slab, magenta accent stripe, stamp
glyph, sans bold), V3 playful dashed postcard (stickers, italic
Cormorant, pill CTA). The demo now actually shows three visually
different directions instead of three similar cards.
Antidote (task b): drop the duplicated pattern tabs (content lives on
/anti-patterns for the full list). Keep the Gallery of Shame as the
signature visual and add a "Browse the full catalog" link underneath.
Section is roughly 40% shorter vertically.
Live Mode demo (task faithful): rewrite to match the real picker flow
one-for-one. Now two separate bars:
- Global bar: dark pill, bottom of the frame, persistent for the
whole session. / + Pick (active) + Detect + DESIGN.md + divider + ✕.
- Contextual bar: LIGHT paper-backed pill that floats above the
picked element, mirroring buildConfigureRow() in live-browser.js.
Dark command pill, transparent input, ×N count, magenta Go button.
Morphs through configure → generating (spinner) → cycling (prev /
counter / next / discard / Accept) → accepted (green ✓ written to
source).
Contextual bar positions itself below the target when space allows,
else above. New JS positionCtx() mirrors the real positionBar()
logic. prefers-reduced-motion still freezes on a representative
"cycling, variant 3" still.
Timeline unchanged in steps but step captions updated to match the
new two-bar split ("Contextual bar appears." etc.).
Two homepage tweaks:
- Section 02 (The Language) gets a pill-style Palette / Periodic view
toggle at the top-right of the intro row. Palette (fisheye command
gallery) is default; Periodic (the periodic table) is behind a tab
click. Collapses the section's vertical footprint substantially
without losing the signature periodic-table visualization.
- Section 04 renamed "Why Impeccable" → "The Case". Matches the
editorial voice of "The Language" / "The Antidote" / "The Editors"
(changelog signature). Sticky nav label Case.
Section 05 is now "Live Mode" with a self-contained real-DOM animated
demo loop. Browser chrome + stage + picker bar + simulated cursor,
running through the full Live Mode flow:
hover → outline → click → open command picker → pick "delight" →
type "more playful" → draw stroke → Go → generating → three
variants cycled → accept → write-to-source confirmation → reset
Three distinct card variants morph in place so the cycling reads as
real iteration, not a slideshow of the same card. Picker bar states
(idle / configuring / generating / cycling / accepted) are driven by
data-phase and matching CSS selectors. Simulated cursor animates
between targets on an expo-out curve; annotations draw a scribble
stroke and pop a comment tag. IntersectionObserver gates the timeline
so it only plays while visible, and prefers-reduced-motion freezes
on a cycling / variant 3 still.
Supporting row below the frame adds the three honest limitations:
frameworks supported (Vite/Next/SvelteKit/Astro/Nuxt), writes to real
source via HMR, CSP-strict apps get a one-time dev-only patch.
Top-nav rename "Visual Mode" → "Overlay" (points at /visual-mode
which is still the overlay-focused sub-page). Sticky section-nav
updated too.
New file: public/js/components/live-demo.js with the timeline driver.
Three polish items on the Why Impeccable section:
- Move the "04 / Why Impeccable" section header into the left column
next to the tab list. The right-side panel now aligns vertically
with the heading instead of sitting offset below it.
- Replace the outer border-top + per-tab border-bottom with an
adjacent-sibling combinator (li + li). No more hairline above the
first tab or below the last one.
- Rework the active-state progress bar. Previously the is-active
border-left-color was fully magenta, which hid the countdown
animation underneath it. Now the active state is driven entirely
by a 2px progress column that either sits full-height (paused or
user-selected) or fills from top to bottom over the cycle window
on a gentle ease curve. The countdown is actually visible now.
Default browser focus-visible ring was a heavy blue outline that
clashed with the dark capsule. Inject a palette-aware inner box-shadow
ring (accentSoft + accent) scoped to the global bar buttons via a
one-time <style> tag. Keeps keyboard focus visible without the
visual noise.
Previously Escape while picking just hid the highlight and set state
to IDLE, leaving the global bar's Pick button visually active. Next
pick attempt fell into a broken state where the button looked on but
no picker ran. Route through togglePick() so the flag, the button,
the UI, and the state all flip together.
Small copy fix on Why panel 04: Figma stamp now reads "last touched
Q3 2025" (was 2024; should reflect closer to the current calendar).
Previous hardening emitted a 7-line todo array and a 10-line ASCII-bar
stderr banner on every accept event, both printed to the agent's
transcript. Per-event overhead added up fast on multi-variant sessions.
Keep the three-layer defence but shrink the per-event noise:
- todo is now a single string: "REQUIRED before next poll: carbonize
cleanup in FILE. See reference/live.md ..."
- stderr is one line with the same pointer.
- reference/live.md keeps the full five-step checklist (loaded once
per session, so its verbosity is a fixed cost — no repetition tax).
Attention signal still triple-redundant: stdout todo, stderr line,
reference section.
After an LLM-triggered session where the carbonize cleanup got skipped
entirely (the instruction was buried as a single bullet among four
cases in live.md, and `_acceptResult.handled: true` felt like a
"done" signal), add three redundant reinforcements:
1. live-accept.mjs now emits a `todo` array on the event payload when
carbonize is true, listing all five cleanup steps plus a pointer to
reference/live.md. The agent reads this as part of the event JSON.
2. live-poll.mjs prints a loud multi-line stderr banner on every
carbonize=true event. Even agents that parse only stdout JSON see
stderr output and can't treat the event as handled without action.
3. reference/live.md pulls the carbonize branch out of the "Handle
accept" bullet list into a dedicated "Required after accept
(carbonize)" section with a numbered five-step checklist, a
rationale paragraph for why skipping is bad, and an explicit
"do not poll again until the file is clean" instruction.
The three layers are deliberately redundant: a future LLM that ignores
the reference text should still be caught by the stderr banner or the
in-event todo, and vice versa.
Per reference/live.md's carbonize instructions (which I should have
followed immediately after the accept event):
- Moved the inline @scope CSS into public/css/workflow.css, rewritten
to use the real .why-visual--v2 class instead of
[data-impeccable-variant="2"].
- Removed the <style data-impeccable-css="afb9794d"> block from index.html.
- Removed both impeccable-carbonize-start / -end marker comments.
- Unwrapped the <div data-impeccable-variant="2" style="display: contents">
wrapper around the accepted variant content.
- Dropped the dead @scope rules for variants 1, 3, 4 entirely.
Only load-bearing element classes remain (why-visual--v2, v2-side,
v2-label, v2-canvas-*, v2-new-*) and they're all styled via the
central stylesheet now.
Alignment: .why-section had its own max-width + horizontal padding,
which made the whole section sit inside a narrower column than every
other section. Drop the extra wrapping; match the .antidote-section /
.visual-mode-section pattern (padding: 2xl 0, border-top).
Panel 04: scrap the "your IDE + AI agent" browser-chrome surface
(confusing, implied a code editor with a fake browser top bar, and
never mentioned what the point actually is). Replace with two side-by-
side tiles: dark "Your AI agent" tile + warm "Your browser / Live Mode"
tile, joined by a magenta "+" glyph. The line underneath lands the
point: two surfaces you already have open, zero handoff.
Addressing feedback that the first round didn't sell the differentiation:
01 Intentional design: scrap the "generic AI card vs warm card" comparison
(the slop card actually looked good, which undermined the point). Show
PRODUCT.md as a real file with fields filled in (Users, Register,
Brand voice, Anti-references) and a pulse-dot footer signaling it's
loaded on every command. The point is that Impeccable knows specific
things about the product before it designs anything.
03 Production codebases: expand the terminal from a single "reading
DESIGN.md" line to a full codebase walk. Tokens from tokens.css,
merged tailwind theme, 34 components listed with variants inferred,
DESIGN.md as one of many inputs. DESIGN.md is shallow; the real work
is traversing source.
04 Where you code: scrap the card grid. Replaced with a two-row
waterfall-vs-unified flow. Old way (greyed, strikethrough): Figma →
Handoff → Ticket → Engineer → QA drift. New way: a single surface
with Design / Code / Ship chips inside a fake IDE chrome. Pulls the
real argument instead of just showing logos.
05 DESIGN.md: picker-sidebar-style rich tiles. Accent color with
8-step tonal ramp, Aa specimens for display + body, component chips +
input preview. Stitch spec badge up top. Matches the visual vocabulary
of the live-mode DESIGN.md sidebar.
Panels 02, 06, 07 unchanged per feedback that they were working.
Per-panel storytelling visualizations, pure HTML/CSS, no image assets:
- 01 Intentional design: "Generic AI" dark/purple gradient card vs.
warm editorial card with /impeccable vocabulary side by side.
- 02 Brand and product, both: tiny brand mock (italic display headline)
vs. product mock (mono/stats rows).
- 03 Production codebases: dark terminal showing /impeccable polish
reading DESIGN.md tokens and component APIs.
- 04 Where you code: prompt bar with blinking caret + 4×2 grid of
harness logos (Claude, Cursor, Codex, Gemini, Copilot, Antigravity,
Kiro, OpenCode).
- 05 DESIGN.md: a file-view of the six Stitch sections with a
"Stitch spec" badge, plus an interop tagline.
- 06 CI/CD: terminal showing `impeccable detect` failing CI with
three issues and exit 1.
- 07 Chrome extension: browser chrome + floating extension popup
listing detections and two magenta outline boxes over "page content".
Auto-rotation: 7s per tab, pauses on hover, stops entirely on any
click/keyboard interaction (user-initiated navigation wins). Thin
magenta progress bar animates on the active tab's left accent as
the rotation progresses. IntersectionObserver gates the whole timer
so it only runs while the section is on screen. prefers-reduced-motion
disables the auto-rotation and the progress animation.
Dropped the "Seven reasons..." lead line.
New section 04 with a vertical tab list on the left and a panel on the
right. Seven items covering the actual differentiators:
1. Intentional design — PRODUCT.md + DESIGN.md + shape as the opposite
of one-shot-mock tools.
2. Brand and product, both — the register split, called out against
frontend-design-style single-mode skills.
3. Production codebases — reads your design system, daily-driver.
4. Where you code — IDE-native, no canvas, no handoff.
5. DESIGN.md — Google Stitch spec-compliant, interoperable.
6. CI/CD-ready — CLI + JSON output for PR gates.
7. Chrome extension — 25 checks, one click, any page.
Section numbers bumped: Visual 04→05, Install 05→06, Changelog 06→07,
FAQ 07→08. Sticky nav updated to match.
Arrow-key tab navigation, aria-selected, hidden attr on inactive
panels, subtle fade on switch, respects prefers-reduced-motion.
Vertical layout on desktop, stacked on mobile.
The DO/DONT extractor in readPatterns stopped finding anything after
the skill went human-authored (new SKILL.md uses plain bullets, one-word
section headings, no DO:/DON'T: prefixes). Replace the extractor with
a hand-curated category list: six categories, three or four DOs and
DON'Ts each. Editorial tone, tight, deliberately a teaser — the full
catalog still lives on /anti-patterns.
The legacy SKILL.md parser is retained in the file as _legacyReadPatterns
in case we want to revive it later with a different format.
Small CSS fix: .faq-question was display:flex with justify-content:
space-between, so inline <code> in a summary got treated as its own
flex item and pushed apart. Switched to relative+absolute positioning
so the + icon sits in the right margin and text flows naturally.
Changelog font size reduced to 0.9375rem for the expanded v3.0 entry.
New FAQ entry on pinning standalone commands back. Pin section added
to /docs/impeccable editorial.
- v3.0 changelog now covers 6 items (was 2): Live Mode, PRODUCT.md,
DESIGN.md (with Google Stitch spec compliance), brand/product
registers, plus the existing consolidation + pin/unpin.
- New FAQ entry answering "how do I get /critique back" via pinning.
- Added Pin commands back as shortcuts section to /docs/impeccable
editorial so it's findable outside the FAQ.
- Reduced changelog item font size (0.9375rem) to match length.
- Swept user-facing copy: .impeccable.md -> PRODUCT.md, removed three
stale npx impeccable live references (the CLI subcommand no longer
exists; live is /impeccable live inside the skill).
- Historical v1.5.0 changelog entry preserved as-is (period-accurate).
Three related site scroll bugs:
1. initAnchorScroll and initHashTracking both called scrollTo with
`behavior: 'auto'`, which defers to CSS `scroll-behavior`. Because
sub-pages.css set `html { scroll-behavior: smooth }`, every anchor
jump and reload-hash-restore animated — despite a code comment
explicitly stating "Instant anchor scroll — no smooth scrolling".
Switch to `behavior: 'instant'` so the JS wins.
2. The reload-hash restore used a fixed `setTimeout(100)` to compute
target position. At 100ms, async Google Fonts (Cormorant Garamond
italic) has not swapped in, so `getBoundingClientRect().top` is
computed against fallback metrics and mislanded by hundreds of
pixels. Retry on `document.fonts.ready` and on window `load`.
3. Remove `scroll-behavior: smooth` from sub-pages.css entirely —
it was silently fighting the JS and made long-page anchor clicks
feel sluggish.
The inline pre-restore wasn't actually fixing a timing issue — the
fix was the fonts.ready + load retries. Since live.js's own
top-level block runs before DOMContentLoaded and we can do the same
retries there, we don't need an inline script injected into every
user page. Simpler HTML, single source of truth.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
scrollTo(y) clamps to the current document.scrollHeight, which is
several hundred pixels short of the final value until async-loaded
fonts swap in (Cormorant Garamond italic grew consulting-section
layout by ~585px in the logs). The initial synchronous scroll was
clamping to ~6165 even though the Go-time target was 6749.5.
Retry on document.fonts.ready and on the window load event, both of
which fire once the document reaches its final height.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
external live.js is fetched, so by the time it runs the browser has
already queued its reload-scroll animation and history.scrollRestoration
='manual' has no effect. Inject a tiny inline synchronous <script> into
the same block live-inject writes, BEFORE the external live.js tag. It
sets scrollRestoration='manual' and does window.scrollTo(0, savedY)
during HTML parse — before the browser can animate anywhere.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
startScrollLock calls stopScrollLock at the top as a reset. I had
clearScrollY() inside stopScrollLock, so every Go sequence was:
writeScrollY(6749.5) → startScrollLock → stopScrollLock → clearScrollY
— the persisted value was wiped right after being written, so resume
after reload read null and locked to 0.
Move clearScrollY to the three genuine session-end sites (hideBar
error path, confirmed/accept, cleanup/discard). stopScrollLock no
longer touches persistent storage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three concrete bugs from the diagnostic logs:
1. saveSession was writing scrollY alongside state, so every call during
resumeSession clobbered the Go-time value with whatever the browser
had left us at (typically 0). Move scrollY to its own localStorage
key, touched only at Go and on user-scroll reanchor.
2. history.scrollRestoration='manual' was being set inside init() at
DOMContentLoaded — by then the browser has already started animating
its restore, especially with scroll-behavior: smooth on html. Apply
it at script parse time, and apply the saved scrollY immediately
there too, before the browser's animation starts.
3. Corrections only fired on MutationObserver. A programmatic smooth
scroll (browser restore animation, or another script calling
scrollIntoView) produces zero DOM mutations — so we never caught it
walking scrollY from 0 up to 4800+ in the recorded session. Snap
back on every scroll event, gated by a 250ms user-gesture window so
we don't fight real user scrolls.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Log target-Y at Go, every mutation that triggers a correction (with the
mutation type + added nodes), every correct-or-noop (with from/to/delta),
every reanchor, and every external scroll event >5px. Lets us see which
step is actually moving the page during wrap / variant insert.
Element-based scroll tracking broke every time: Bun's HMR destroys the
target element, the browser's scroll anchoring picks a different nearby
element (e.g. the #downloads CTA) as its new anchor, and the page jumps
to wherever that surviving element is. My element-based correction then
computes against a replaced DOM node with stale / wrong geometry.
The primitive the user actually cares about is window.scrollY — they
want the page to stay where it is, regardless of which element survives
the patch. Pin scrollY directly: capture it at session start, restore it
on every mutation inside the wrapper, re-anchor on user scroll, store it
in saveSession for reload-resume.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two things were wrong. First, I capped large corrections — which was
backwards: a huge delta is exactly when we most need to restore (it
means the browser's own scroll anchoring drifted, which is what makes
the page 'jump to Get Started' when Bun's HMR destroys and re-inserts
our target). Remove the cap so any delta is corrected.
Second, the browser's built-in scroll anchoring was competing with us:
when Bun destroys our target element, the browser picks the nearest
surviving element (like a CTA anchor in another section) as its new
scroll anchor and scrolls to keep THAT stable. Disable overflow-anchor
on html and body for the duration of the session so we own scroll
entirely; restore the original values on stopScrollLock.
Kept the user-scroll grace window (400ms): wheel / touch / arrow keys
re-anchor and suppress corrections, so momentum scrolls don't get fought.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Watching document.body caught every mutation on the page — shader
animations, Bun HMR indicators, tooltips, anything — and fired a
correction on each one, which fought the user when they tried to scroll
mid-session. Now the observer only responds to mutations inside the
session's wrapper. On user scroll intent (wheel / touchstart / touchmove
/ arrow & page keys), cancel any pending rAF correction and re-anchor
to the element's new position, so momentum scrolls don't get yanked
back by a stale correction.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The reload-only fix missed the primary case: Bun's HTML loader hot-patches
the DOM in place rather than doing a full page load, so the resume
codepath never ran and the browser's scroll drifted wherever Bun's patch
left it. Likewise variant cycling (taller → shorter) and agent-driven
variant inserts both mutate layout without a reload.
Add a scroll lock: on Go (and on resume after a true reload), capture the
selected element's viewport-top and install a MutationObserver on body
that re-measures the target and corrects scroll on every batch of DOM
mutations. The target is re-resolved each pass via sessionId + visible
variant, so it survives DOM swaps that invalidate `selectedElement`.
Scroll intent events (wheel, touchstart, arrow/page keys) re-anchor to
the new position so we never fight a user who scrolls during a session.
Also set `scrollRestoration = 'manual'` at init so true reloads don't
land the user somewhere odd before our correction runs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When HMR misses and we fall back to window.location.reload(), the native
scroll restoration landed the page somewhere near the right region but
not on the selected element, because layout had shifted between the
save and the reload. Capture the element's getBoundingClientRect().top
into the session snapshot, disable native scroll restoration on resume,
and manually scroll the element back to that exact viewport-relative
position. Run a second correction pass after fonts and images settle to
absorb late layout shifts without animating the fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Screenshotting a transparent container rendered black because we were no
longer passing `backgroundColor` to modern-screenshot at all (to avoid
its `background-color !important` override on elements with their own
bg, like the teal card). That fix left elements without their own bg
rendering on a transparent canvas, which reads as black wherever the
PNG is previewed.
Now we resolve per-element: if the element has an opaque
background-color or a background-image, omit the option (element's own
bg renders, no override). If it's transparent, walk up ancestors to the
first opaque background (falling back to body/html) and pass that as
the canvas fill.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. cleanup-deprecated: strip `i-` prefix before fingerprint lookup so
`i-harden` / `i-optimize` classify correctly (regression from the
prefixed-naming migration).
2. build: substitute `{{scripts_path}}` in reference/*.md the same way
it's substituted in SKILL.md. Previously the placeholder survived
unresolved in built reference files, so any reference that told the
agent to run a scripts path emitted a literal `{{scripts_path}}` to
the shell.
3. live-poll: drop the `undici` import. Node's built-in fetch enforces a
300s headers timeout that can't be lowered per-request, so we now cap
each poll slice at 270s and loop internally until a real event or the
caller's total timeout. Removes the hard `ERR_MODULE_NOT_FOUND`
failure when undici isn't transitively hoisted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
modern-screenshot force-sets `background-color: X !important` on the root
clone's inline style when backgroundColor is passed, clobbering the
element's real background and rendering every captured card with the page
body color. Omit the option so the canvas stays transparent and the
element's own background renders into the foreignObject.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaced during hands-on testing against a real Next 16 + Turbopack app
(EACManagement). All four compound to produce unusable live iteration
for React users; fixed bottom-up because each one blocked testing the
next.
## 1. Picker bar snaps to (0,0) on first variant arrival
In startVariantObserver, `showVariantInDOM(sessionId, 1)` hides the
original via display:none but we never re-pointed selectedElement.
Next frame, getBoundingClientRect() on the hidden original returns a
zero rect and the bar positions at (0,0). Clicking Next masked the
bug because cycleVariant already calls updateSelectedElement.
Fix: after showVariantInDOM, re-point selectedElement via
pickVariantContent(wrapper, visibleVariant) — same call the no-HMR
fallback and updateSelectedElement already use.
## 2. React NotFoundError on accept/discard (Next 16 / Turbopack)
handleAccept and cleanup both called
`wrapper.parentElement.replaceChild(...)` eagerly, before the agent's
source rewrite had propagated through HMR. That yanks children out
from under React's reconciler; when React later tries to remove/replace
the wrapper, its fiber tree no longer matches the DOM and it throws.
Fix, both paths:
- cleanup (discard): `wrapper.style.display = 'none'` so variants
disappear immediately, no structural DOM mutation.
- handleAccept: skip the eager replaceChild entirely. The accepted
variant is already the only visible child of the wrapper thanks to
the display: contents pattern; HMR cleans up the wrapper itself.
- Both paths schedule a 2s fallback replaceChild that runs only if
HMR hasn't cleaned up — keeps static-server / no-HMR flows working.
- Capture sessionId + visibleVariant in closure variables before the
1800ms cleanup timer zeros them, so the fallback still has context.
## 3. Server serves stale live.js forever
loadBrowserScripts() read live-browser.js once at startup into a
liveScript string. The /live.js handler served that cached string
with no cache headers. Every edit to the browser script was invisible
until a full server restart — silently broke the iteration loop on
fixes#1 and #2 for the user.
Fix:
- loadBrowserScripts returns { detectScript, livePath } — existence
check only, no caching.
- /live.js handler re-reads livePath on every request and prepends
__IMPECCABLE_TOKEN__ / __IMPECCABLE_PORT__ each time.
- Response headers: Cache-Control: no-store, no-cache, must-revalidate,
max-age=0 + Pragma: no-cache.
detect.js stays cached — it rarely changes during a session.
## 4. Picker stuck in GENERATING when HMR doesn't fire
The only 'done' fallback fired when arrivedVariants === 0 and called
injectVariantsFromSource, which parses raw source via DOMParser. That
can't work for TSX/JSX/Vue/Svelte — JSX expressions aren't valid HTML.
If HMR flaked or was slow (500+ line inserts on Next 16 are prone to
this), state stayed in GENERATING and the spinner ran forever.
Fix: give HMR a 2s grace window, then `window.location.reload()`.
resumeSession already counts variants off the rendered DOM on load
and transitions straight to CYCLING — reload is the universal
recovery path that works for any framework, HTML, static server,
anything.
injectVariantsFromSource is now dead code on the 'done' path. Kept
for potential pure-HTML-no-HMR future use.
## Credit
Precise repro + root-cause diagnosis from the other agent in the
EACManagement session. #2 and #4 are the high-impact ones for Next 16
/ Turbopack; #3 is the meta-fix that made iterating on #1 and #2
possible at all.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Schema caught up after the editorial→brand rename. Without this,
the skill loader falls through to the "missing field — infer from
content" branch every session. Impeccable itself is firmly in the
brand register — distinctiveness is the bar, the voice lane is
editorial-magazine (serif + italic + long-form) chosen deliberately
from within that register.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-prod — no need to carry forward the backwards-compat line in
SKILL.md or the historical note in CLAUDE.md. Existing PRODUCT.md
files with `register: editorial` will hit the "missing field" branch
and get re-inferred from content, which is fine for the tiny number
of projects that touched it during iteration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
"editorial" was doing semantic double duty — naming the strategic
distinction (design IS the product) AND a specific visual aesthetic
(editorial magazines, broadsheets, serif display, italic drop caps).
Models pattern-matched the aesthetic and defaulted to it on every
brand brief, producing magazine-shaped landing pages for hiking
brands, tech tools, restaurants.
The register name now describes the SURFACE KIND, not an aesthetic.
Brand covers every visual lane — tech-minimal, luxury, editorial-
magazine, consumer-warm, brutalist-grid, hand-drawn — each with
legitimate voice within the register.
## Changes
- `reference/editorial.md` → `reference/brand.md`. Content rewritten:
broadened typography guidance (pairing shapes per brand genre,
single-family commitment is valid), broadened color references
(Stripe, Vercel, Liquid Death alongside Klim, Condé Nast), added
a second slop test ("name your aesthetic lane") to prevent drift
into editorial-magazine defaults, added brand ban against the
drift itself.
- SKILL.md: register names brand/product; load brand.md.
- teach.md: register values brand/product; signals renamed; example
principles no longer use "editorial over marketing" phrasing.
- Six sub-commands (animate/bolder/colorize/delight/layout/quieter):
per-register subsections flipped Editorial: → Brand:.
- product.md: cross-references updated.
- live.md: register reference updated; density axis no longer uses
"editorial" as a synonym for "dense".
- typeset.md: per-register paragraph generalised beyond serif+sans
pairing.
- CLAUDE.md: architecture section rewritten; kept "editorial
wrapper" content-authoring term as-is (different meaning).
## Legacy handling
- `editorial` is accepted as an alias for `brand` on PRODUCT.md's
register field — agents treat it as `brand` without asking.
- Documented in SKILL.md setup section and CLAUDE.md.
## What's unchanged
- Register identification priority (task cue → surface → PRODUCT.md).
- Permission structure (brand can go big, product stays restrained).
- Shared design laws, absolute bans, color strategy vocabulary.
- Framework fixtures and tests.
Full build clean, test suite passes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The insert path puts the block's opener line right after the anchor's
indent (e.g. six spaces plus </body>), which transfers the indent
onto the opener line and leaves the anchor unindented in the injected
file. The remove path consumed the block's trailing newline but left
the pre-block indent behind, producing two bugs in one:
Before insert: ` </body>`
After remove: ` \n</body>` (orphan indent + unindented anchor)
Fix: capture `([ \t]*)` immediately before each marker and replace the
whole block (including its trailing newline and any trailing spaces on
the ender line) with just the captured indent. The indent now hands
itself back to the anchor line that follows — the file round-trips
byte-for-byte.
New tests/live-inject.test.mjs with four round-trip cases:
- HTML file with indented </body>
- JSX layout with indented </body> (EAC shape)
- Multi-file batch
- Column-0 </body> (no indent — already worked; regression baseline)
All four pass after the fix. Full suite clean via `bun run test`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Gemini 3 Flash baseline showed the Unsplash bullet wasn't directive
enough — the model still dropped imagery entirely on italian-
restaurant and vintage-moto-forum niches when the brief clearly
implied photography.
Changes:
- Added a MUST-ship-imagery lead paragraph listing the niches that
require photography (restaurant, hotel, magazine, etc.).
- Gave a literal Unsplash URL shape (`images.unsplash.com/photo-{id}?
auto=format&fit=crop&w=1600&q=80`) with real photo-id examples, so
weaker models have a concrete pattern to copy rather than inferring
the URL format.
- Promoted "zero imagery on an imagery brief" into the Editorial bans
list so it lands as a hard rule, not a nudge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three related extraction bugs surfaced in the EAC session all rooted
in the line-based state machine:
1. `<style ... />` (JSX self-closing) had no separate `</style>` for
the "skip until close" mode to exit on, so the state machine stuck
and every `data-impeccable-variant` marker after it got missed.
Accept reported `handled: false, error: "Variant N not found"`.
2. A variant whose entire `<div ...>...</div>` sits on one line had
its body silently discarded — the marker line was `continue`d past,
and the extractor started capturing from the next line, which
usually belonged to a different variant or the wrapper close.
3. `extractCss` kept scanning for `</style>` after a self-closing
opener, greedily swallowing every subsequent variant div as "CSS".
Result: a mangled carbonize block stuffed with HTML and a duplicate
variant rendered below.
## Fix
Replaced the line-based state machine with a string-based flow:
- `stripStyleAndJoin(lines, block)` returns the wrapper text with
`<style>` elements fully removed. Handles self-closing, same-line
open+close, and multi-line open/close. Markers inside CSS strings
(e.g. `@scope ([data-impeccable-variant="1"])`) are gone by the
time extraction runs — no false positives.
- `extractInnerByAttr(text, attrMatch)` is a balanced-tag matcher that
walks the joined text finding `<TAG ...attrMatch...>…</TAG>` with
proper depth tracking for nested same-tag elements. Handles
single-line, multi-line, and deeply nested variants.
- `extractOriginal` and `extractVariant` are thin wrappers over the
above.
- `extractCss` gets explicit same-line handling: returns null for
self-closing (nothing to carbonize), extracts inner content via
regex for same-line `<style>…</style>`, falls through to the
existing multi-line path otherwise.
## Tests
New tests/live-accept.test.mjs with four cases — all failing before,
all passing after:
- Self-closing `<style />` with dangerouslySetInnerHTML
- Single-line `<style>…</style>`
- Multi-line `<style>...</style>` (regression baseline)
- Discard restores the original element after self-closing style
Wired into `bun run test`. Full suite passes.
Credit: precise repro + root-cause trace from the other agent in the
EAC session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five related bugs that surfaced in a real Next.js App Router project
(EAC) all rooted in live-wrap.mjs treating source as line-anchored HTML:
1. findElement matched on raw substring anywhere, so it landed on a
className continuation line of a multi-line JSX tag whose class
happened to collide with a later target. The wrong tag got wrapped
(really, its attribute line got wrapped, producing broken JSX).
2. findClosingLine's opener regex required whitespace or `>` after the
tag name, so a bare `<section\n className="..."\n>` opener was
unrecognised; it returned `start` silently, capturing only one line.
3. buildSearchQueries only emitted `class="..."`, missing React's
`className="..."`. The full-combo query never fired in JSX, so
search silently degraded to single-class substring matching.
4. Wrapper output used `style="display: contents"` unconditionally,
which is invalid JSX (type error in strict setups, parser hazard
in production transforms).
5. --tag was ignored during the primary class search. Ambiguous class
hits inside the wrong element type weren't filtered out.
## Fixes
- New OPENER_RE `/<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/` recognises
tag openers at end-of-line too.
- New findOpenerLine(lines, matchLine, tag): walks up to 10 lines
backward to the enclosing opener when the match lands on a
continuation line. Aborts the walk if it hits a different tag.
- findElement now iterates all matches (not just the first), takes
a tag parameter, and routes through findOpenerLine; wrapCli passes
--tag through.
- buildSearchQueries emits both `class="..."` and `className="..."`
for multi-class queries, and both `<tag class="..."` /
`<tag className="..."` for tag+class combos.
- Wrapper builder emits `style={{ display: "contents" }}` when
commentSyntax is JSX and `style="display: contents"` otherwise.
- findClosingLine uses the same OPENER_RE so its tag-name extraction
works on multi-line openers too.
## Tests
Five new regression tests in tests/live-wrap.test.mjs, all failing
before the fix, all passing after:
- wraps the correct <section> when a class collides with a multi-line
tag elsewhere
- emits JSX-safe style attribute ({{ }}) in .tsx files
- finds elements via className= (React) when the exact class combo is
unique there
- respects --tag to reject matches inside the wrong element type
- findClosingLine recognises an opener line where the tag sits at
end-of-line (multi-line JSX)
31/31 in tests/live-wrap.test.mjs and 54/54 in
tests/framework-fixtures.test.mjs pass.
Credit: precise bug report from the other agent in the EAC session
made diagnosis and test design straightforward.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The content heuristic for deciding whether a deprecated skill dir belongs
to us returned false for harden and optimize (their v2.x SKILL.md never
said "impeccable"), while lock-entry cleanup used the authoritative
source field. Result: lock entries purged, dirs orphaned.
Layer three signals now: lock source (authoritative), word heuristic,
then per-skill description fingerprints for the two stock v2.x skills
that predate the self-identification convention.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Shape names renamed to describe the patch mechanism (what the agent does)
rather than the origin (where the CSP lives). One template now covers
multiple frameworks.
## Shape rename
- shared-helper → append-arrays
- inline-headers → append-string
append-arrays applies wherever CSP is a structured directive array.
append-string applies wherever CSP is a literal value string.
## New detection coverage
- SvelteKit kit.csp.directives in svelte.config.js → append-arrays
- Nuxt routeRules / nitro.routeRules CSP header → append-string
- Nuxt-security module's contentSecurityPolicy → append-arrays
## New fixtures
- sveltekit-csp/: SvelteKit config with kit.csp.directives. Includes
expected-after-patch.js showing the array spread.
- nuxt-csp/: Nuxt 3 config with routeRules CSP. Includes
expected-after-patch.ts showing the string splice.
## Skill docs
Single append-arrays template covers Next monorepo, SvelteKit, and
Nuxt-security. Single append-string template covers inline Next
headers() and Nuxt routeRules. Per-framework specifics listed as
sub-bullets under each shape.
54 tests across 9 fixtures, all passing. Clean fixtures (plain vite,
nextjs-app, astro, sveltekit, multipage-with-generator) still classify
as shape: null.
Astro and Vue (non-Nuxt) left unhandled by design: Astro has no
first-party CSP mechanism; Vue without Nuxt is covered by the existing
Vite fixture. Plain Svelte has no framework CSP primitive and inherits
from its bundler (Vite/Rollup).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Real-world tests (EAC Next turborepo) confirmed that CSP is the common
blocker for live mode. Adds setup-time detection with a one-time user
consent flow — the patch becomes a permanent, dev-guarded entry in the
user's own config, not a transient add/remove.
## Changes
- New detect-csp.mjs helper: grep-based classifier returning
{ shape, signals }. Shape is one of:
- "shared-helper" (monorepo CSP helper with additional*Src arrays)
- "inline-headers" (literal CSP string in headers())
- "middleware" (response.headers.set in middleware.ts; detect-only v1)
- "meta-tag" (<meta http-equiv>; detect-only v1)
- null (no CSP)
Max depth 6, skips node_modules / build / cache dirs, 64KB per file.
- cspChecked boolean on config.json. First-run setup runs detection;
subsequent runs skip. Users re-trigger by deleting the flag.
Validator accepts it.
- Skill live.md gains:
- CSP detection step in first-time setup (gated by cspChecked)
- Consent-prompt template (so every agent phrases it the same way)
- Shape 1 patch template: append `...__impeccableLiveDev` to
additionalScriptSrc/additionalConnectSrc in the app's config
- Shape 2 patch template: two-point edit — declare a dev-only
variable, interpolate into script-src and connect-src in the
CSP literal string
- Troubleshooting note for "said no but now live doesn't work"
## Fixtures
- nextjs-turborepo/: Turborepo shape (shared CSP helper with
additionalScriptSrc options). Sanitized from a real monorepo so the
patch mechanics get tested against realistic layering. Includes
expected-after-patch.ts for human/agent review.
- nextjs-inline-csp/: app-level next.config.js with a literal CSP
string. Includes expected-after-patch.js showing the Shape 2 edit.
## Tests
Framework-fixture harness extended with a detect-csp shape-classification
assertion per fixture. 42 tests across 7 fixtures pass. Clean fixtures
(vite-react, nextjs-app, astro, sveltekit, multipage-with-generator)
correctly return shape: null.
## Deliberately not doing
- No patches[] array, no marker-based rollback, no add/remove lifecycle.
The patch is a permanent dev-guarded config line — the same kind of
edit a user would make themselves.
- No base URL rewriting or proxy mechanism. Script tag still points at
localhost:8400; CSP permits it once patched. No browser-side changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Top-level .gitignore excludes dist/ broadly, which silently dropped the
multipage-with-generator fixture's files/dist/*.html from the previous
commit. The fixture tests need those files on disk to copy into the
tmp repo and assert is-generated behavior — without them, the test
suite fails on a fresh clone.
Added a negation pattern that re-includes tests/framework-fixtures/**/dist/
paths. The real dist/ output directories elsewhere in the repo remain
ignored.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five representative project shapes under tests/framework-fixtures/ that
stage into fresh tmp git repos and drive the live scripts against each:
- vite-react: tracked index.html shell + src/App.jsx
- nextjs-app: app/layout.tsx as JSX inject target
- astro: src/layouts/Layout.astro
- sveltekit: src/app.html shell + src/routes/+page.svelte
- multipage-with-generator: src/ tracked, dist/ gitignored (our own
repo's shape); exercises the is-generated guard and
element_not_in_source fallback
Each fixture declares its config, expected source/generated paths, and
wrap cases in fixture.json. The harness copies into tmpdir, applies
gitignore, commits, then asserts:
- inject --port lands the script tag at the correct anchor across all
configured files
- inject --remove strips it cleanly
- is-generated classifies source vs generated paths correctly
- wrap routes to the expected source file or emits the expected
fallback error
Plumbing + bug caught while building out the matrix:
- IMPECCABLE_LIVE_CONFIG env var so tests can point live-inject at a
fixture-specific config.json without clobbering the harness copy.
Backwards-compatible.
- live-wrap.mjs no longer hardcodes dist/build in its directory skip
list. Only node_modules and .git remain universal skips; the
isGeneratedFile check is now the sole guard for generated paths. This
lets the includeGenerated second pass find elements in dist/ and
report generatedMatch, which is what the multipage-with-generator
fixture needs to exercise.
Wired into bun run test. 25 tests, 5 suites.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses every issue surfaced during hands-on live-mode testing.
## Injection across multi-page sites
- Config schema: `file` → `files: string[]` so multi-page static sites can
opt into script-tag injection across every HTML entry the browser loads.
- `live-inject.mjs` loops the array, reports per-file results, and
refuses silently with `config_invalid` if the schema is stale.
- `insertBefore` switched from first-match to last-match (lastIndexOf)
so the anchor lands at the true close of `</body>`, not the first one
embedded inside a `<pre><code>` documentation sample.
## Source-vs-generated detection
- New `is-generated.mjs` helper: gitignore check + generated-header
markers. Edge-case `generatedFiles` config dropped — the two real
signals cover every project shape we tested.
- `live-wrap.mjs` excludes generated files from auto-search and returns
clear fallback errors: `file_is_generated`, `element_not_in_source`
(with `generatedMatch` path), and `element_not_found`.
- `live-accept.mjs` refuses to persist into generated files; returns
`mode: "fallback"` so the agent takes over via the Handle fallback
flow.
## Accept correctness
- `extractVariant` / `extractOriginal` now skip `<style>` regions when
matching markers. Previous regex substring match treated
`@scope ([data-impeccable-variant="N"])` in CSS as the target HTML
div, capturing garbage and producing orphan CSS that rendered as
prose on the page.
- On accept, the chosen variant's content is wrapped in
`<div data-impeccable-variant="N" style="display: contents">` so the
carbonize block's `@scope` selectors keep matching. Users see the
accepted design immediately; no pre-carbonize dead state.
## Browser-side UI
- `positionBar` gains a third case: when the selected element is taller
than the viewport, pin the bar to a stable viewport anchor instead of
teleporting between top and bottom as the user scrolls.
- No-HMR source-fetch path (`injectVariantsFromSource`) now calls
`hideShaderOverlay()` on state transition to CYCLING. Previously the
shader kept running after variants arrived via the fetch fallback.
- `pickVariantContent` helper replaces fragile `> :first-child`
selection for outline positioning. Skips non-visual tags (style,
script, link, meta, template) and falls back to the variant div
itself when a variant contains multiple visual children.
- `resumeSession` re-captures and restarts the shader overlay when
the page reloads mid-generation (Bun HTML HMR does a full reload
and destroys the canvas).
- MutationObserver re-anchors `selectedElement` when the original
element is detached by HMR, preventing zero-rect highlight drift.
## Skill docs
- `live.md` reframes `config.files` as "the HTML files the browser
actually loads" and documents the regen-wipes-inject caveat for
multi-page generator projects.
- New Handle fallback section covers the three wrap error shapes and
how the agent should manually wrap for preview and commit to real
source on accept.
- Handle accept documents the new `data-impeccable-variant` wrapper
and the carbonize agent's duty to strip it.
## Prefetch feature (landed but disabled)
A `prefetch` event fires from the browser on first CONFIGURING per
route so the agent can pre-Read the source file before Go. Real latency
win in the linger-before-Go case but costs a harness round trip when
Go fires quickly. Disabled via a `PREFETCH_ENABLED = false` flag in
`live-browser.js`; server validator and skill dispatch stay so re-
enabling (with a browser-side debounce) is a one-line change.
## Harness guidance
Earlier skill rewrite compressed two load-bearing instructions:
- Restored prescriptive wording for "open the tab via Chrome MCP
before the first poll" and the Claude Code background-poll policy.
- Flag-mapping for `live-wrap` rewritten as explicit bullets so models
don't collapse `--element-id`/`--classes`/`--tag` into a single
`--query` argument.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bun's HTML HMR does a full page reload when the live-wrap.mjs edit
lands, so the shader canvas is destroyed and in-memory capture blob is
lost. resumeSession rehydrated state from localStorage but never
restarted the overlay, so the wait went dead.
resumeSession now re-captures the original's content (still in the DOM
inside the variant wrapper) and restarts showShaderOverlay when we
reload mid-generation. Also swaps the two remaining :first-child
selectors in resumeSession for pickVariantContent so the earlier
loose-children robustness fix carries across reloads.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two regressions surfaced in smoke testing, both traceable to state
drift when live-wrap.mjs rewrites the source file and HMR swaps the
DOM.
1. Variant outline on the wrong element. The skill rewrite lost the
explicit "each variant must be a complete element replacement"
rule and dropped the "full element replacement" comments from
variants 2 and 3. Models started producing variants with loose
sibling children, so live-browser's :first-child selector framed
only the first sibling. Restored the rule, made all three comments
consistent, and replaced :first-child with pickVariantContent —
which skips non-visual tags (style/script/link/meta/template) and
falls back to the variant div itself when a model still ships
multiple visual children.
2. Loading shader freezes after wrap. The MutationObserver only woke
up when new non-original variants arrived, so when the wrapper
first appeared via HMR with just the original inside, selectedElement
was left dangling on the now-detached pre-wrap node. Scroll-tracking
read a zero rect on every frame and collapsed the shader canvas to
0x0. The observer now re-anchors selectedElement to the original's
content the moment the wrapper shows up, keeping overlays positioned
until real variants land.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>