Commit Graph
215 Commits
Author SHA1 Message Date
Paul BakausandClaude Opus 4.7 ac2237a7a9 feat(skill): identity-first variant planning in /impeccable live
Restructures live.md Step 4 into four phases: identity extraction
(Phase A, non-skippable, falls back to CSS variables and computed
styles when DESIGN.md is absent), default vs departure mode pick
(Phase B), variant planning by primary axis or aesthetic lane
(Phase C), and a family-level squint test before the sentence
pass (Phase D). Default mode preserves identity and varies
expression; departure mode only triggers on explicit signals
(PRODUCT.md anti-references calling out the current surface, or
the user prompt asking for departure).

Adds reflex-reject aesthetic lanes to brand.md as a parallel to
the existing font reflex-reject list. Editorial-typographic is
the first entry. Expands SKILL.md's category-reflex check to two
altitudes (theme + palette from category, then aesthetic family
from category + anti-references).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 11:31:56 -07:00
Dan DocaandGitHub c332c7aa91 feat(load-context): resolve context dir outside repo root (#119) (#123)
Adds a configurable lookup path for PRODUCT.md / DESIGN.md / DESIGN.json so
they don't have to live at the project root. Resolution order (first match
wins):

  1. process.env.IMPECCABLE_CONTEXT_DIR (absolute or relative to cwd)
  2. cwd, when canonical or legacy files are at the root (back-compat)
  3. Auto-fallback subdirs of cwd: .agents/context/ then docs/
  4. cwd as a default "no context found" location

Existing layouts (PRODUCT.md / DESIGN.md at repo root) keep working unchanged
- step 2 preserves the current behaviour. The auto-fallback covers the two
most common conventions seen in the wild (.agents/context/ for AGENTS.md
auto-import setups, docs/ for the request in the issue) without needing any
configuration.

Changes:
  - load-context.mjs: export resolveContextDir() and use it inside
    loadContext(); add contextDir to the JSON output
  - live-server.mjs: import resolveContextDir and read PRODUCT.md /
    DESIGN.md / DESIGN.json from the resolved dir instead of process.cwd()
  - SKILL.md: short note on the env var and fallback dirs in Setup -> Context
  - tests/load-context.test.mjs: 19 cases covering env var, fallbacks,
    legacy migration scope, and back-compat

Legacy .impeccable.md -> PRODUCT.md auto-migration stays scoped to cwd root;
fallback dirs are read-only as far as auto-rename is concerned.

Closes #119
2026-04-29 08:52:40 -07:00
9a5d0e71a9 fix(live): switch live-poll to execFileSync, validate ids strictly (#124)
* fix(live): switch live-poll to execFileSync, validate ids strictly

live-poll.mjs built the live-accept invocation with execSync and string
interpolation of event.id and event.variantId. Both fields originate in
the browser; validateEvent only checked truthiness, so shell metacharacters
in either field would land in the shell-parsed command.

Real exploitability is gated by the per-session token (loopback only,
unguessable UUID), so risk is low. The construction itself is structurally
unsafe though, and the fix is small.

- live-poll.mjs: execSync(string) → execFileSync('node', argv). Drops the
  hand-rolled single-quote wrap for --param-values; execFileSync passes
  each arg as a discrete argv slot, no shell parsing.
- live-server.mjs validateEvent: tighten id and variantId to match the
  actual generator shapes (8 hex chars and 1-3 digit numeric strings).
  Defense in depth so any value reaching downstream code is inert by
  construction.
- live-server.test.mjs: add three regression tests covering accept/discard
  rejection of shell-metachar ids and non-numeric variantIds. Update the
  three existing fixture ids to match the new pattern.

Reported in #122.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: refresh pnpm-lock.yaml to match package.json

Cloudflare Pages runs pnpm install --frozen-lockfile and was failing on
ERR_PNPM_OUTDATED_LOCKFILE: the lockfile was missing entries for
@ai-sdk/anthropic, @ai-sdk/openai, @anthropic-ai/claude-agent-sdk,
@anthropic-ai/sdk, @google/genai, ai, modern-screenshot, zod, and had
stale specifiers for jsdom, marked, playwright, wrangler, puppeteer.

Drift was introduced when package.json was last edited without a lockfile
regen. Running pnpm install --lockfile-only resolves it; verified with
pnpm install --frozen-lockfile (clean install succeeds).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Paul Bakaus <paulbakaus@pauls-mbp-3.lan>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:04 -07:00
Paul BakausandClaude Opus 4.7 1f760aff61 fix(live): expandReplaceRange handles multi-line self-closing JSX <div />
Cursor Bugbot review on 8660d3a flagged a real corruption bug:

> Multi-line self-closing div breaks depth tracking in expandReplaceRange.
> The forward div-depth walk applies openRe / selfCloseRe / closeRe
> per-line. A multi-line `<div\n  className="spacer"\n/>` causes openRe
> to match the opener line but selfCloseRe fails on both lines because
> `/<div\b[^>]*\/\s*>/` requires the full tag on one line. Depth is
> permanently over-counted by 1, so the walk overshoots.

Trace on the JSX-marker-inside-wrapper layout:
- Inside the wrapped element, a multi-line `<div … />` increments depth
  at the `<div` line and never decrements.
- Forward walk's depth never returns to 0 → end stays at block.end (the
  inner marker comment) → replace range stops there.
- Wrapper's outer `</div>` is left orphaned in the file after
  accept/discard, breaking the JSX. Worse: an unrelated subsequent
  `<div className="next-card">…</div>` sibling gets its `</div>`
  mis-counted as the wrapper close, and the depth walk corrupts further.

Fix: rewrite the forward walk on JOINED text instead of per-line. A
single regex `/<div\b[^>]*?(\/?)>|<\/div\s*>/g` spans newlines (because
`[^>]` matches `\n`), so it correctly identifies multi-line opens,
closes, AND self-closes. Convert the match offset back to a file line
index to set `end`. Walk-back logic for the wrapper opener is
unchanged.

Test coverage:
- New `expandReplaceRange handles multi-line self-closing <div />` test
  in live-accept.test.mjs constructs the exact Bugbot scenario: a
  multi-line `<div\n  className="spacer"\n/>` inside the picked
  element AND an unrelated `<div className="next-card">After</div>`
  sibling right after. Asserts the discard removes ALL impeccable
  markers / wrapper attrs, preserves the next-card sibling intact, and
  the multi-line `<div />` survives inside the restored content.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:19:07 -07:00
Paul BakausandClaude Opus 4.7 8660d3aa22 fix(live): wrap shape-of-output bugs from second Bugbot review
Two more Cursor Bugbot findings on commit 11dfad81:

1. `filterByText` short-text returned the wrong sentinel value.
   When the trimmed snippet was shorter than 8 chars, the function
   returned `candidates.slice()` (all candidates). The caller then
   sees `filtered.length > 1` and fires `element_ambiguous` — exactly
   the opposite of the documented short-text fallback ("caller falls
   back to first-match," which corresponds to `filtered.length === 0`).
   So any picker event with a short textContent on a page with multiple
   matching siblings spuriously errored.
   Fix: return `[]` for short text, matching the JSDoc.

2. `endLine` in the wrap output was wrong for multi-line picked elements.
   `wrapperLines.length` counts ARRAY elements, but one element is a
   `\n`-joined multi-line string (originalIndented). The actual
   wrapper-region row count is `wrapperLines.length + (originalLines.length
   - 1)`. Reporting `endLine = startLine + wrapperLines.length` placed
   the boundary inside the wrapper for any multi-line pick, giving
   downstream agents an incorrect range.
   Fix: add the originalLines offset (matching what `insertLine` already
   does after the prior commit).

Test coverage:
- `short --text falls back to first-match instead of erroneously firing
  element_ambiguous` covers fix #1.
- `returns endLine that includes the multi-line original content offset`
  covers fix #2 by wrapping a 5-line <section> in a real HTML file and
  asserting the reported endLine points at the variants-end marker (and
  the next line is </main>, proving no rows were missed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 16:52:02 -07:00
Paul BakausandClaude Opus 4.7 11dfad81da fix(live): CSP-meta patch+revert preserves space before self-closing /
Sanity-check on the live-inject unwrap path turned up a real round-trip
bug on HTML files that ship a `<meta http-equiv="Content-Security-Policy"
content="..." />` tag (the leading space before `/>` is the canonical
self-closing form).

Trace:
- The tag-finder regex (`<meta\s+([^>]*?)\/?>`) captures any whitespace
  between the last attribute and the closing `/>` as part of `attrs`.
- patchCspMeta did `attrs.replace(content, newContent) + ' ' + marker`,
  appending the marker AFTER that captured trailing whitespace. Result:
  `...content="..."  data-...="..."` — a double space inside attrs and
  the original space-before-slash gone.
- revertCspMeta then strips the marker via `\s*${origAttr.full}`, which
  greedily eats both spaces — so the round trip leaves `"/>` with no
  space, even though the original was `" />`.

Fix: split off the trailing whitespace from `attrs` before patching,
splice the marker into the attribute body with a single leading space,
and re-append the original trailing whitespace. The marker-removal
regex then consumes exactly one space and the trailing space rides
through unchanged.

Test coverage:
- New `round-trips through CSP-meta patch and revert` test in
  live-inject.test.mjs covers the canonical Vite shape (CSP meta with
  ` />`).
- Plus a `round-trips with insertAfter` test for symmetry — the existing
  suite only covered insertBefore.
- Existing 4 round-trip tests (HTML, JSX layout, multi-file, column-0)
  all still pass byte-for-byte.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 16:03:14 -07:00
Paul BakausandClaude Opus 4.7 a701ee613a fix(live): wrap preserves relative indent of multi-line picked elements
Companion to the prior outer-indent fix. live-wrap.mjs's
`originalLines.map(l => indent + '    ' + l.trimStart())` calls
`trimStart()` on every line, which strips ALL leading whitespace and
collapses multi-line picked elements to a uniform indent. So a 6/8/6
shape like

    <aside className="card">
      <h1 className="hero-title">Hero</h1>
    </aside>

was being reindented to 10/10/10 inside the wrapper, and on
accept/discard the round-trip restored 6/6/6 — the <h1> ended up at
its parent's depth instead of nested inside it.

Fix: extract `minLeadingSpaces(lines)` and strip only the COMMON
minimum across the picked lines before reindenting under the wrapper.
That mirrors how `deindentContent` on the accept side already works,
so wrap+accept now form a clean round-trip.

Test coverage:
- Expanded the indent regression test in live-accept.test.mjs to
  also assert the inner `<h1>` at 8-space indent and the closing
  `</aside>` at 6 — proving the relative depth survives wrap and
  discard end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:59:07 -07:00
Paul BakausandClaude Opus 4.7 99e68370b9 fix(live): JSX accept/discard restores at original indent (Bugbot review)
Cursor Bugbot caught this on PR #118 review:

> JSX discard/accept restores content with wrong indentation. In the JSX
> path, `indent` is captured from `lines[block.start]` — the marker comment
> line inside the wrapper div, which is indented 2 extra spaces relative
> to the original element. But `expandReplaceRange` expands the replacement
> to include the outer `<div data-impeccable-variants>` wrapper, which sits
> at the original element's indent level. `deindentContent(original, indent)`
> restores content to the marker's deeper indent, so all restored lines end
> up 2 spaces deeper than the original element was.

I'd actually noticed the symptom during the live testing session ("some
odd indentation in card-2 after discard") and dismissed it as cosmetic.
Bugbot's analysis matches exactly.

Fix: anchor the deindent base on `replaceRange.start` instead of
`block.start`. For HTML the two are identical (markers sit outside the
wrapper), so HTML is unchanged. For JSX `replaceRange.start` is the
outer `<div>` at the original element's indent — correct base.

Also dropped a duplicate `expandReplaceRange` call in handleAccept that
the earlier edit left orphaned.

Test coverage:
- Two new regression tests in live-accept.test.mjs:
  - `discard restores JSX content at the original indent` runs the
    real wrap CLI and asserts the restored <aside> opener lands at
    its original 6-space indent (was 8 before the fix).
  - `accept (no carbonize, raw HTML) restores at the original indent
    on JSX` exercises the same anchor on the accept path.
- Inner-element indent loss inside the wrapped content (`<h1>` ending
  up at the same indent as its parent `<aside>`) is a separate,
  pre-existing wrap behavior — left for a follow-up; explicitly
  noted in the test comments.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:55:27 -07:00
Paul BakausandClaude Opus 4.7 fdb9e7c6f8 fix(live): screenshot overlay no longer flashes solid black during loading
Same alpha-string trap pattern as the recent detectPageTheme fix, on a
different code path. resolveCanvasBackground walks parents looking for
an opaque background; on a page that doesn't set its own bg the loop
runs out and fell through to:

  return getComputedStyle(document.body).backgroundColor
    || getComputedStyle(document.documentElement).backgroundColor
    || '#ffffff';

`getComputedStyle(body).backgroundColor` for a default-bg page returns
the literal string "rgba(0, 0, 0, 0)" — non-empty, truthy — so the `||`
chain short-circuits to transparent-black instead of falling through to
'#ffffff'. modern-screenshot then composites the capture onto a black
canvas; the WebGL shader overlay flashes solid black until the shader
finishes loading.

Fix: drop the buggy fallback. The while-loop already covered <body> and
<html>; if neither is opaque the only sensible answer is the browser's
default canvas color (white).

Test coverage:
- New tests/live-browser-regression.test.mjs pins the anti-pattern
  with a static-source check (live-browser.js is an IIFE with no module
  exports, so this is the cheapest reliable regression guard). Also
  pins the equivalent guard for detectPageTheme's readOpaque helper
  added in the prior commit.
- Wired the new test file into `bun run test`'s explicit list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:40:24 -07:00
Paul BakausandClaude Opus 4.7 9ec904302b fix(live): textContent disambiguation handles missing inter-element whitespace
While driving the new live loop end-to-end against the repeated-aside
fixture, --text disambiguation silently fell back to first-match instead
of landing on the picked card.

Root cause: `el.textContent` concatenates child text nodes without
inserting whitespace, so `<h1>Hero Two</h1><p>Second card body copy.</p>`
reads as "Hero TwoSecond card body copy." — but the source has whitespace
between </h1> and <p>. The single-space normalization on both sides
missed the join boundary; substring comparison failed; filterByText
returned [] and the caller fell through to first-match.

Fix: filterByText now compares both single-space AND no-whitespace
normalizations on each side, accepting the candidate if EITHER matches.
Bumped the minimum-target-length threshold from 6 to 8 to compensate
for the slightly looser comparison.

Plus two doc clarifications surfaced during the same session:

- live.md now warns that variant CSS using bare `:scope { ... }` styles
  the variant wrapper div, not the picked element. Always use a
  descendant combinator (`:scope > .card`, `:scope .hero-title`, etc.) —
  the fake test agent's CSS is the canonical template.
- live.md documents the agent-side abort path. Aborting an in-flight
  generate via `live-accept --discard` only mutates source — the browser
  bar stays in GENERATING forever. Use `live-poll --reply EVENT_ID error
  "msg"` instead so the browser receives the error SSE and resets.

Test coverage:
- New unit test in live-wrap.test.mjs covering the textContent-without-
  inter-element-whitespace shape (three identical <aside> branches each
  with <h1> + <p>, picks the second by --text).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:03:49 -07:00
Paul BakausandClaude Opus 4.7 54d9f05ea5 fix(live): land valid TSX through wrap → preview → accept → carbonize
Closes #114.

Three orthogonal bugs that surfaced together when live mode picked an
element inside a Vite React/TSX component with sibling branches:

1. JSX wrapper insertion produced invalid TSX
   - Replacing a single picked JSX child with [comment, <div>, comment]
     yields three adjacent siblings, which oxc rejects with "Adjacent
     JSX elements must be wrapped in an enclosing tag."
   - A Fragment `<></>` solves the adjacency case but breaks
     `cloneElement`-using parents (Radix `asChild`, Headless UI, etc.)
     with "Invalid prop supplied to React.Fragment."
   - Fix: keep the wrapper `<div data-impeccable-variants="ID">` as the
     single JSX-slot child and tuck both marker comments INSIDE it.
     accept/discard now expands its replacement range to include the
     wrapper's `<div>` open/close lines via div-depth tracking.

2. carbonize produced nested template literals in TSX `<style>`
   - extractCss captured `{` / `` `} `` lines from the agent's existing
     `<style>{`…`}</style>` template, then handleAccept re-wrapped with
     another pair, producing `<style>{`{`@scope…`}`}</style>` which oxc
     rejects with "Expected `}` but found `@`".
   - Fix: extractCss now strips a leading `{` and trailing `` `} ``
     wherever they appear in the captured content (own line OR attached
     to the first/last CSS line), so re-wrapping always yields exactly
     one `{` ` … ` `}` pair.

3. Ambiguous source matching for repeated JSX branches
   - `findElement` returned the first substring match. Multiple
     `<aside className="card">` siblings all matched the same query, so
     wrap silently landed on the first regardless of which one the user
     picked.
   - Fix: live-wrap accepts `--text TEXT` (the picked element's
     textContent), collects ALL candidates via `findAllElements`, and
     narrows by a tag-stripped, JSX-expression-stripped substring match.
     Returns `element_ambiguous + candidates[]` when multiple branches
     match equally; falls back to first-match when source uses dynamic
     content (`<h1>{title}</h1>`) so existing flows aren't broken.
   - The fake e2e agent now forwards `event.element.textContent` to
     wrap, and live.md tells the agent to do the same.

Test coverage:
- New `vite8-react-tsx-repeated-aside` e2e fixture: three identical
  `<aside>` branches, picks the second card's <h1>, runs the full
  wrap → Go → cycle → accept → carbonize cycle on a real Vite + TSX
  dev server, asserts that Hero One and Hero Three survive untouched
  (proving wrap landed on the correct branch).
- Six new unit tests across live-wrap.test.mjs and live-accept.test.mjs
  covering the Fragment-replacement design, both leading/trailing
  template-literal placements, --text disambiguation, the dynamic-
  content fallback, and the element_ambiguous error shape.
- New `runtime.assertSourceContains` fixture hook so other regression
  fixtures can assert sibling-branch survivability cheaply.

All 186 unit + static-fixture tests pass; all 21 live e2e fixtures
(20 prior + new TSX) pass with no console errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 13:23:20 -07:00
Paul Bakaus 5f5e2b013d Release impeccable skill v3.0.4 2026-04-28 00:16:30 -07:00
Paul Bakaus 54f6ccf6f0 codex in auto-review became way too autonomous - significantly harden craft/shape flows 2026-04-27 23:41:23 -07:00
Paul BakausandClaude Opus 4.7 630e586b01 feat(live): make picker chrome modal-host friendly (Radix, Headless UI, vaul)
Closes #113.

Picker chrome could become unclickable inside Radix Dialog portals, and
clicking it dismissed the host dialog. Three orthogonal issues surfaced
during manual verification:

1. Modal-aware chrome
   - Add `defangOutsideHandlers` and apply it to bar, picker, params
     panel, annotation overlay, global bar, and design panel host.
   - Sets `pointer-events: auto !important` on interactive chrome so
     Radix's `body { pointer-events: none }` modal scroll-lock can't
     silence our UI.
   - Stops `pointerdown` / `mousedown` / `focusin` propagation at the
     chrome boundary so DismissableLayer / FocusScope outside-handlers
     never fire for clicks that land on us.

2. detectPageTheme: misread transparent body as black
   - `getComputedStyle(body).backgroundColor` returns `rgba(0,0,0,0)`
     when no bg is set; the prior regex captured (0,0,0) and ignored
     alpha, calling every default-bg page "dark."
   - Honor alpha, walk body → html, fall back to
     `prefers-color-scheme` only when both are transparent.

3. Exit X invisible on host pages with `button { padding: ... }`
   - Every other chrome button sets padding inline; exitBtn didn't.
     Host resets like `button { padding: 0.5rem 1rem }` (in the new
     fixture, common in the wild) inflated the 24x24 button into 56x40
     and pushed the SVG into a non-rendering region — DevTools showed
     the right styles, the X just didn't paint.
   - Pin `padding: 0` + `box-sizing: border-box`, match the toggle
     icon spec (14 / stroke 1.5 / textDim → text on hover).

4. Toast no longer obscures the global bar
   - Position the toast above globalBarEl's actual rect instead of a
     fixed bottom: 16px that overlapped the bar's bottom: 14px.

Test coverage: new `vite8-react-radix-dialog` fixture exercises the
full pick → Go → cycle → Accept loop with `@radix-ui/react-dialog`
+ `Portal` + `Overlay` + `Content`. Without the fix, clicking Go
dismisses the dialog and unmounts the picked element. All 20 live
e2e fixtures pass; all 180 unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 16:22:00 -07:00
vivshaw ceb0ef8f67 chore: rename all references Neon Mirai -> Neo Mirai to match site title 2026-04-26 19:10:46 -04:00
Paul BakausandClaude Opus 4.7 6e96f62803 fix(live): readable freeform input on dark bar + tools/live-loop.mjs
The configure row's text input filled its background with translucent
magenta (BP.accentSoft) on focus. Composited against the dark bar surface
this produced a murky purple where the browser's default placeholder
gray washed out — flagged in a real session as "godawful styling, gray
text on dark magenta really hurts my eyes". Fix: focus state shows an
accent-colored border only, no fill; placeholder color is set explicitly
to BP.textDim via a one-shot stylesheet so it reads in both themes.

tests/live-e2e/agent.mjs: runAgentLoop's wrapTarget now accepts either a
static {classes,tag,elementId} (test fixture mode) OR a function that
derives the target from each generate event (real-use mode where the
picked element is unknown ahead of time).

tools/live-loop.mjs: standalone runner that attaches the LLM agent to a
running live-server. Used as a test-harness shortcut for validating live
mode out of band; in production the user's coding agent (Claude Code,
Cursor, etc.) plays this role directly via the live skill spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 01:35:32 -07:00
Paul Bakaus 74f16d6310 Add Neon Mirai showcase 2026-04-25 01:23:51 -07:00
Paul BakausandClaude Opus 4.7 4310352423 fix(live): variant observer detects wrappers added as descendants
startVariantObserver's "dominated" check only matched when the variant
wrapper was added directly as a mutation's addedNode. SvelteKit (and any
framework whose HMR replaces a whole subtree on edit) adds the wrapper as
a descendant of an added <main> or similar — the observer ignored those
mutations and the session stayed in GENERATING forever even with all 3
variants present in the DOM.

Surfaced by the LLM-agent E2E run on vite8-sveltekit. The fake-agent path
masked the issue because its splice timing happened before Vite's reload
finalized; the slower LLM call shifted timing into the failure window.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 01:05:28 -07:00
Paul BakausandClaude Opus 4.7 89ffd73d4b improve(polish): make polish a true superset of retired /normalize
Aligning to the design system is now non-optional, drift gets named by
root cause (missing token / one-off / conceptual), and a new Information
Architecture & Flow dimension covers the user-flow shape that polish
previously left to chance. Folds the missing pieces from the deprecated
normalize skill into the v3.0.1 changelog bullet rather than a new bump.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 00:56:35 -07:00
Paul BakausandClaude Opus 4.7 c3e18fe664 fix(live): four bugs surfaced by E2E suite + CSP meta auto-patch
CSP meta-tag auto-patch (live-inject.mjs)
  When the user's HTML carries <meta http-equiv="Content-Security-Policy">,
  the cross-origin load of /live.js and the SSE/POST stream back to
  localhost:PORT are both blocked. Insert: append http://localhost:PORT to
  script-src and connect-src, plus blob: to img-src (the shader overlay),
  stash the original content value as a base64 data-impeccable-csp-original
  attribute. Remove: decode the marker and restore the original verbatim.
  Header-based CSP (Next/Nuxt/SvelteKit configs) intentionally untouched —
  those flow through the existing detect-csp.mjs reference path.

JSX-aware accept (live-accept.mjs)
  - Carbonize stash now emits style={{ display: 'contents' }} for JSX targets
    instead of style="display: contents" (HTML form). React 19 was throwing
    "Failed to set indexed property [0] on CSSStyleDeclaration" on the
    string form because it iterated chars onto the style object.
  - extractCss now matches </style> anywhere on a line, not just at line
    start. Previously a JSX template-literal close like `}</style> would
    leak the backtick + brace into the carbonize stash, breaking JSX.
  - Carbonize stash wraps the CSS body in {` … `} for JSX targets so curly
    braces in CSS rules don't get parsed as JSX expressions.

Conditional-render UX (live-browser.js)
  - Drop the 2s-then-window.location.reload() fallback in the SSE 'done'
    handler. That reload was masking a real failure mode: when the picked
    element lives inside conditional render (closed modal, hidden tab,
    other-route), Fast Refresh remounts the parent and state resets, so
    the variants land in source but never reach the DOM. Reload also reset
    state to default, leaving the user stuck.
  - Replace with a 6s contextual toast: "Variants ready. If the picked
    element isn't visible, retrace the path that revealed it — they'll
    appear automatically." The MutationObserver stays armed and
    auto-transitions to CYCLING once the variants finally mount.
  - Pick-time heads-up: when the picked element is inside [role="dialog"],
    [data-state="open"], a multi-tab tabpanel, or an aria-expanded
    collapsible, fire a brief upfront toast so the user knows what to
    expect if state resets during generation.

Hydration race (live-browser.js)
  - SvelteKit (and any framework that hydrates after HTML parse) was
    failing post-Vite-page-reload because init() ran resumeSession()
    before the variant wrapper hydrated into the DOM. The OLD reload
    fallback masked this by triggering a second reload whose hydration
    benefited from warm cache. Without that, fix it properly: install a
    scout MutationObserver in init() that retries resumeSession() once
    [data-impeccable-variants] lands in the DOM.

Shader overlay (live-browser.js)
  - WebGL fallback in showShaderOverlay used Object.assign(img.style,
    canvas.style, …), which throws on modern Chromium because
    CSSStyleDeclaration's indexed properties are not writable. Use
    cssText to copy positioning instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 23:37:45 -07:00
Paul BakausandClaude Opus 4.7 0760cdf3e9 fix(skill): update stale SKILL.md font-tag reference in typography.md
typography.md pointed at SKILL.md's `<font_selection_procedure>` and
`<reflex_fonts_to_reject>` XML tags, which were removed in the v3
consolidation and moved into brand.md as the "Font selection procedure"
and "Reflex-reject list" sections. Agents loading typography.md via the
craft flow were chasing content that no longer existed. Now points at
brand.md with correct section names.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 18:03:57 -07:00
Paul BakausandClaude Opus 4.7 a42d21856c fix(skill): resolve cursor bot findings on colorize + critique
colorize.md: the brand-register paragraph claimed "a dominant color can own
the page" and "accent rate stays ≤10%" in the same breath. SKILL.md scopes
the ≤10% rule to Restrained only; Committed / Full palette / Drenched
exceed it on purpose, and brand.md explicitly encourages those strategies.
Rewritten to defer to the color-strategy ladder.

critique.md: two cross-references still pointed at "Step 4" / "Step 5"
after those headers were renamed to "Ask the User" / "Recommended Actions".
Swapped the references to the new names so the flow is self-consistent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 17:06:09 -07:00
Paul BakausandClaude Opus 4.7 5613891aa6 docs(typography): absorb tactical additions from typecraft-guide-skill
Merged ten tactical items from ehmo/typecraft-guide-skill into the typography
reference at the upstream author's request: dark-mode weight/tracking/leading
compensation, font-display: optional vs swap, preload-critical-weight-only,
variable fonts for 3+ weights, clamp() max-to-min ratio bound, container/
font-size coupling to preserve measure, text-wrap: balance / pretty,
font-optical-sizing: auto, quantified ALL-CAPS tracking (5-12%), and the
paragraph-rhythm rule (space OR indent, never both).

Skipped: platform-specific tables (iOS/Android/print), confidence markers,
severity-graded report format, academic sources, and the punctuation
subsection (em-dash prescription conflicts with the project copy rule).

Attribution lives in NOTICE.md, not inside the skill content.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 16:56:29 -07:00
Paul BakausandClaude Opus 4.7 e0ab3a73b7 feat(live + site): preserve variant attr on accept, designing-page redesigns
Runtime fix in live-browser.js: the 2s static-server fallback in handleAccept
now swaps the outer wrapper with the `[data-impeccable-variant="N"]` div itself
(+ display:contents), matching what live-accept.mjs writes to disk. Scope rules
anchored on the variant attribute keep matching on the non-HMR path, so the
accepted design no longer flashes unstyled until reload. Propagated to all
harness script copies.

/designing:
- §03 Polish redesigned as drenched magenta masthead: commands live in the band,
  three title/description columns beneath on cream.
- §04 Maintain redesigned as architectural poster diptych: extract + document
  vizzes become the hero element, caption below.
- §05 Interop section removed.
- §05 (was §06) "Pick a register" renamed to "Brand, or product." with a
  two-lane hairline-divided layout and an auto-selected framing in the sub.

Live mode status: BETA → ALPHA across the periodic table, magazine spread,
and docs callout, reflecting real-world-testing readiness.

Skill bootstrap: removed the `<post-update-cleanup>` block from source/SKILL.md
(the source repo is the origin; running cleanup-deprecated here would touch
legitimate source). CLAUDE.md and AGENTS.md now document the skip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 16:44:46 -07:00
Paul Bakaus df72ca2a58 docs(impeccable): clarify live mode param expectations for freeform
Section 7 no longer reads as default-zero: composition-sized targets,
freeform bias toward 1–2 dials on non-tiny surfaces, hard cap of four.
Cross-link freeform to §7 in the action loader; sync all harness copies.

Made-with: Cursor
2026-04-23 13:28:35 -07:00
Paul BakausandClaude Opus 4.7 c160ffc38d feat(live): v2 sidecar upgrade + preserve per-project config on build
Unify the design-system panel's data shape around DESIGN.md frontmatter
as the primary source of truth; the sidecar carries only what Stitch's
frontmatter schema can't (extensions + live component HTML + narrative).
Also fix a long-standing build bug that destroyed per-project config.

Shape changes:
- Server /design-system.json now returns { parsed, sidecar, hasMd,
  hasSidecar, mdNewerThanJson, parseError?, sidecarError? }. No more
  mode switching; both layers ship when present and the panel merges.
- Panel consolidates renderSidecarVisual + renderParsedMdVisual into a
  single renderDesignVisual that merges frontmatter primitives with
  sidecar extensions.colorMeta / typographyMeta. Helpers for color,
  typography, radii model-building. Parsed-md narrative synthesis
  survives as a fallback when no sidecar.
- DESIGN.json rewritten at schemaVersion 2: extensions.{colorMeta,
  typographyMeta, shadows, motion, breakpoints} + components (with
  refersTo pointing back to frontmatter component keys) + narrative.
  Token primitives no longer duplicated in the sidecar.

Build fix:
- scripts/build.js:634 wiped .claude/skills/ (and every other harness
  dir) on each rebuild, then recopied from dist. After commit b0feed0
  unbundled per-project config.json from dist, the sync destroyed the
  user's live-mode config on every build without replacing it.
- Added stashPerProjectArtifacts / restorePerProjectArtifacts in
  scripts/lib/utils.js. Hoisted PER_PROJECT_SCRIPT_ARTIFACTS to a
  module-level export so build.js and readSourceFiles share one
  source of truth. Build now preserves config.json across the sync.

Verified in browser: panel renders 10 colors, 9 typography roles, 3
shadows, 6 grouped components, 9 rules, 25 do/don't items, all merged
correctly from frontmatter + v2 sidecar with zero console errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 12:09:55 -07:00
Paul BakausandClaude Opus 4.7 a5cd7bf859 refine(skill): harden critique against skipping sub-agents and [Human] overlay
Two loopholes in the critique procedure let the model shortcut past its own rigor: "SHOULD delegate" with a broad "if sub-agents are not available" escape, and "Browser visualization (when available)" framing that made the [Human] detector-overlay tab read as optional color. Both get rationalized away under context pressure even though the isolation is what makes the combined score honest and the overlay is the user-facing deliverable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 11:02:39 -07:00
Paul BakausandClaude Opus 4.7 e1cfba0f25 feat(design): align DESIGN.md with Stitch's frontmatter spec
Adopt the Stitch google-labs-code/design.md format's two-layer shape:
YAML frontmatter carries machine-readable tokens, prose body covers the
six canonical sections. The sidecar DESIGN.json now extends frontmatter
rather than duplicating it.

- document.md: new frontmatter section, Step 2b staging, sidecar scoped
  to extensions, Do's/Don'ts now cite PRODUCT.md anti-references by name,
  OKLCH vs hex presented as project posture not mandate.
- design-parser.mjs: tiny YAML-subset reader, exposes model.frontmatter,
  schemaVersion bumped to 2, prose-scraping fallback intact.
- live-browser.js: "basic view" CTA copy reflects frontmatter-first model.
- DESIGN.md: add frontmatter with 10 colors, 9 type roles, 7 components;
  OKLCH values direct per The OKLCH-Only Rule.
- tests/design-parser.test.mjs: coverage for no-frontmatter, Stitch-shape,
  missing-terminator, comment handling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 09:30:00 -07:00
Paul BakausandClaude Opus 4.7 b0feed06c9 feat(live): config globs + drift-heal warning for multi-page projects
Config drift was a real tripwire for projects with static generators: new
HTML files get added, never make it into config.files, silently skip
injection. Two additions.

config.files entries now accept glob patterns (**, *, ?) expanded via
fs.globSync in live-inject. Multi-page projects can write
["public/**/*.html"] once and never maintain the list again. New optional
exclude field filters out matched files (email templates, demo fixtures).
HARD_EXCLUDES of node_modules and .git are enforced regardless of user
config so vendor trees can never receive a tracking script.

live.mjs now runs a drift scan after inject: walks common page-source
roots (public/, src/, app/, pages/) and reports HTML files not covered
by the resolved inject targets. Respects user excludes so intentional
omissions aren't flagged. Output JSON carries configDrift: { orphans,
orphanCount, hint } or null. live.md documents the agent flow for
surfacing drift to users without auto-mutating the config.

Unbundle config.json from the distributable skill: it's a per-project
artifact, not skill code. readSourceFiles now skips any PER_PROJECT_ARTIFACTS
during source scan so build output to .claude/ .cursor/ etc never ships
one project's inject targets to another's install. The per-harness
copies stay gitignored via the existing **/skills/impeccable/scripts/config.json
rule; each consuming project writes its own on first /impeccable live.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 09:12:56 -07:00
Paul BakausandClaude Opus 4.7 6846a135ce refine(live): skip screenshot upload when no annotations present
Without comments or strokes the screenshot is pure visual anchoring,
biasing the model toward the existing rendering and fighting the
three-distinct-directions brief. Local blob still drives the shader
overlay; upload and screenshotPath are gated on annotation presence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 08:19:00 -07:00
Paul BakausandClaude Opus 4.7 2341fe3637 feat(live): tune popover, theme-aware bar/picker, action icons, params system
Adds a coarse-controls ("Tune") popover that slides out from behind the
contextual bar via clip-path, showing 2-4 per-variant knobs (range / steps /
toggle) driven by a `data-impeccable-params` manifest. Range/toggle drive
CSS custom properties on the variant wrapper; steps toggle a data
attribute. Values reset per variant; on accept, current values are passed
through live-poll to live-accept as an `impeccable-param-values` comment
for the carbonize cleanup step to bake in.

Other live-UI work in this change:
- Theme-aware palette (barPaletteForTheme) now drives the contextual bar,
  action picker, and tune popover. Dark sand on light pages, paper on
  dark. Detection has a localStorage dev override for QA.
- Action picker chips get inline SVG icons (wand / bars / funnel / sparkle /
  type ramp / circles / grid / devices / curve / star / bolt) stacked
  above the label; selection state recolors via currentColor.
- Accept button switched to saturated site magenta with paper text.
- Cycle dots reworked: solid accent for active, neutral for arrived,
  hairline ring for pending. No more magenta-on-gray noise.
- Tune chip sits in the cycling row with a count pill badge; open state
  uses accentSoft bg + accent text (no ad-hoc white border).
- Popover uses the bar's palette with a deeper surface (surfaceDeep),
  lives behind the bar via z-index so a 6px overlap reads as tucked under
  it, and animates with clip-path inset() for reliable slide behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 18:17:17 -07:00
Paul Bakaus 96c6565cae Improve image-guided shape and craft flows 2026-04-22 15:57:18 -07:00
Paul Bakaus d7f7df1b80 fix(live): subtle focus ring on global-bar icon buttons
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.
2026-04-22 13:19:08 -07:00
Paul Bakaus 37c1a364a4 fix(live): Escape in PICKING also resets the global bar's Pick button
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).
2026-04-22 13:16:41 -07:00
Paul Bakaus af50f22d57 chore(skill): slim per-event carbonize reminders
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.
2026-04-22 13:12:03 -07:00
Paul Bakaus f38a8d40ee chore(skill): harden post-accept carbonize cleanup so the agent can't skip it
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.
2026-04-22 12:42:10 -07:00
Paul BakausandClaude Opus 4.7 bd86147d70 refactor(live): move scroll-restore out of injected inline back into live.js
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>
2026-04-22 10:55:32 -07:00
Paul BakausandClaude Opus 4.7 f0f2935547 fix(live): retry inline scroll-restore on fonts.ready and load
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>
2026-04-22 10:48:13 -07:00
Paul BakausandClaude Opus 4.7 fb78ec4553 fix(live): inject inline pre-restore script so scrollY wins vs browser
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>
2026-04-22 10:45:39 -07:00
Paul BakausandClaude Opus 4.7 5e04a9f25a fix(live): don't clear scroll key inside stopScrollLock
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>
2026-04-22 10:41:15 -07:00
Paul BakausandClaude Opus 4.7 868d8c4126 fix(live): separate scroll-key, pre-empt browser, snap on every scroll
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>
2026-04-22 10:35:00 -07:00
Paul Bakaus a6aa98c616 chore(live): add diagnostic logging to scroll lock
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.
2026-04-22 10:28:48 -07:00
Paul BakausandClaude Opus 4.7 565381a3e7 fix(live): pin window.scrollY instead of element viewport top
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>
2026-04-22 10:25:40 -07:00
Paul BakausandClaude Opus 4.7 1e533e535a fix(live): disable browser overflow-anchor during session, always correct
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>
2026-04-22 10:20:51 -07:00
Paul BakausandClaude Opus 4.7 b99ab4db2c fix(live): scope scroll lock to session wrapper, let user scroll cancel corrections
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>
2026-04-22 10:15:23 -07:00
Paul BakausandClaude Opus 4.7 ad17880af1 fix(live): observer-based scroll lock holds element at viewport top during session
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>
2026-04-22 10:08:00 -07:00
Paul BakausandClaude Opus 4.7 4f4df85250 fix(live): restore scroll to element's viewport-relative top after reload
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>
2026-04-22 09:58:30 -07:00
Paul BakausandClaude Opus 4.7 101dc50362 fix(live): resolve canvas background from ancestors when element is transparent
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>
2026-04-22 09:47:08 -07:00
Paul BakausandClaude Opus 4.7 1ba75a820e fix(skill): 3 review-bot findings from EAC PR
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>
2026-04-22 09:32:31 -07:00
Paul BakausandClaude Opus 4.7 99494348bf fix(live): don't pass backgroundColor to domToBlob
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>
2026-04-22 02:05:22 -07:00