Qoder ships an Agent Skills system at .qoder/skills/{name}/SKILL.md with
slash-command invocation, mapping cleanly onto the existing transformer
pipeline. Adds Qoder as a 13th first-class harness:
- PROVIDER_PLACEHOLDERS entry in scripts/lib/utils.js (model, config_file,
ask_instruction, command_prefix) mirroring the Pi/Rovo Dev shape.
- PROVIDERS entry in scripts/lib/transformers/providers.js with
configDir=.qoder and the OpenCode/Claude Code frontmatter field set
(user-invocable, argument-hint, license, compatibility, metadata,
allowed-tools), since Qoder docs explicitly support those.
- transformQoder named export in scripts/lib/transformers/index.js for
test-spy parity (kept per CLAUDE.md guidance, even though build.js uses
PROVIDERS directly).
- .qoder added to PROVIDER_DIRS in bin/commands/skills.mjs so the CLI
detects existing Qoder installs.
- HARNESSES.md updated: official docs row, frontmatter support column,
directory structure row, and "Last verified" date bumped.
- DEVELOP.md reference link added.
- .github/ISSUE_TEMPLATE/feature_request.md and PULL_REQUEST_TEMPLATE.md
extended with Qoder in the provider checklists.
- Built .qoder/skills/impeccable/ tree committed (per CLAUDE.md harness
output dirs are tracked so npx skills can read them at install time).
The dynamic providers.test.js loop picks up Qoder automatically; all
non-prefix Qoder cases pass. The pre-existing Windows-only prefix-test
flake affects every provider equally and is out of scope for this PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
- Marketplace source moved from "./" to "./plugin", a thin generated
subtree containing only the plugin manifest and the impeccable skill.
Per-version plugin cache shrinks ~378× (~770 KB instead of ~291 MB),
and the lockfile is no longer included in the source path so the
cache extraction never runs bun install. (#107)
- skills field in plugin.json now ends with a trailing slash to match
the documented schema (code.claude.com/docs/en/plugins-reference,
every directory example uses ./path/). Three reporters converged on
this fix because Claude Code's plugin loader skips command
registration on some setups when the slash is missing. (#86)
- Anti-patterns maintenance agent moved out of .claude/agents/ into
CLAUDE.md / AGENTS.md as concise inline guidance, since it is
repo-internal dev workflow, not user-facing. The plugin was also
the only place this agent was exposed to install users.
- Skills version bumped to 3.0.2 so existing users pick up the new
install path on next /plugin update.
- Top-level harness directories (.claude/skills/, .cursor/skills/, ...)
intentionally stay where they are; npx skills add reads them
directly from the GitHub repo and that path is unaffected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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
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>
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>
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>
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>
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>
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>
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.
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>
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>
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>