A ground-up hardening of live mode, driven by a production session in a nested-app monorepo that hit six distinct failure classes. Full design rationale in docs/LIVE-REWRITE-PLAN.md; every Codex-reported failure now has a mechanical fix and a regression test. Roots: live/roots.mjs resolves appRoot/repoRoot/contextRoot once at boot (keyed on dev-server configs, not monorepo brand markers), persists a manifest, and every live CLI re-anchors onto it at startup, so a helper run from the wrong directory can no longer fork session state. Context files are discovered upward to the git root. Render truth: variant_mounted / variant_mount_failed events give the journal per-variant mount state; failures reach the agent's poll queue, raise a persistent error card with Retry (no more localStorage wipe), and an attach probe names root/dev-server mismatches explicitly. The browser rehydrates from the server when localStorage is gone. Svelte: the scaffolder now parses with the app's own svelte 5 compiler. Control flow survives (an each collection crosses the contract as one structured prop), keyed each blocks hydrate synthetic keys, and anything a detached preview cannot support falls back to source-preview instead of shipping a wrong scaffold. Preview modules live in per-publish revision directories, defeating stale transform caches. Accept: CSS is reconciled, not appended. Matching selectors are replaced, params bake from params.json kinds, the compiler's unused-selector pass prunes superseded rules (pre-existing dead rules protected), a selector- loss postcondition refuses any write that would drop hand-written rules, and live-complete refuses to finish while live plumbing remains in source. Also: framework registry (live/frameworks/) with a crash-safe injection journal, session-store snapshot caching with read-only reads, protocol enum consolidation, steer Send button, honest DESIGN-panel empty states. Testing: new unit suites (roots, AST scaffolder, accept CSS, accept pipeline, framework conformance); e2e now fails on preview-tree 404s, proves computed-style mount for every variant, drives the Tune panel through baked params, and injects failures (broken mounts, republish, storage loss). New runtime fixtures: monorepo-nested-vite (repo root != app root) and vite8-sveltekit-stateful (each blocks + state). Nightly full-matrix cron. An independent adversarial review pass preceded this commit; its blocker and major findings are fixed and regression-tested. This work was produced with AI assistance (Claude Code). Co-Authored-By: Claude Code <noreply@anthropic.com>
13 KiB
Framework fixtures
Representative project shapes for exercising live mode against different framework conventions. Each fixture is a small directory tree that the test harness copies into a temp git repo, then drives live-inject.mjs, live-wrap.mjs, live-accept.mjs, and lib/is-generated.mjs against.
Fixtures can also opt into a runtime E2E pass that actually installs dependencies, boots the framework dev server, and drives a Playwright browser to verify the live handshake. See the runtime block below.
Layout
<fixture>/
files/ project tree the test copies into tmp
gitignore.txt becomes .gitignore in tmp (so we can commit the real files here)
fixture.json config + expected results the test consumes
fixture.json schema:
{
"name": "human-readable label",
"config": { ...contents for .impeccable/live/config.json ... },
"sourceFiles": ["paths that is-generated should classify as source (false)"],
"generatedFiles": ["paths that is-generated should classify as generated (true)"],
"wrapCases": [
{
"name": "description",
"args": { "classes": "...", "tag": "...", "elementId": "..." },
"expectedFile": "where wrap should land (relative to fixture root)",
"expectsError": "optional error code, e.g. element_not_in_source"
}
],
"csp": {
"shape": "shared-helper | inline-headers | middleware | meta-tag | null",
"signals": ["diagnostic hints — paths where CSP was detected"],
"patchTarget": "which file the agent should modify",
"expectedAfter": "filename of the reference post-patch output inside this fixture"
},
"runtime": {
"styling": "plain-css | tailwind-v4 | styled-components | ...",
"appDir": "website",
"install": ["npm", "install"],
"devCommand": ["npm", "run", "dev"],
"scheme": "http",
"ignoreHTTPSErrors": false,
"readyPattern": "Local:\\s+https?://[^:]+:(\\d+)",
"readyTimeoutMs": 120000,
"pickSelector": "h1.hero-title",
"pickPosition": { "x": 10, "y": 10 },
"variantSequence": [3, 1, 2],
"acceptedSourcePattern": "<ul[^>]*class=\"[^\"]*\\bexpense-list\\b",
"assertSourceContains": ["{#each expenses as expense, i}"],
"stateProbe": {
"textSelector": "[data-testid='open-count']",
"expectedText": "3 offen",
"windowProperty": "__impeccableStatefulMounts",
"expectedWindowValue": 1,
"expectWindowUnchanged": true
},
"paramsScenario": {
"variant": 2,
"rangeLabel": "Lead",
"rangeValue": 1.8,
"stepsLabel": "Density",
"stepsOptionLabel": "Snug",
"expectSourceContains": ["line-height: 1.8"],
"expectSourceMissing": ["letter-spacing: 0.14em"]
},
"componentFailureScenarios": { "variant": 2, "storageLoss": false },
"mode": "insert",
"insert": {
"anchorSelector": "section#features",
"position": "after",
"prompt": "Add a testimonial strip below features",
"expectSelector": ".inserted-strip",
"assertAnchorContains": "feature-grid"
},
"preActions": [
{ "type": "click", "selector": "[data-testid='open-modal']" },
{ "type": "goto", "path": "/about" }
],
"reloadProbe": {
"preActions": [{ "type": "click", "selector": "[data-testid='open-modal']" }],
"expectSelector": "h1.hero-title"
},
"steer": {
"message": "steer-e2e mark hero",
"expectSelector": "h1.hero-title[data-impeccable-steer=\"e2e\"]"
},
"probe": {
"expectLiveInit": true,
"expectConsoleClean": true
}
}
}
The expectedAfter file lives alongside fixture.json (not inside files/) and is a human/agent-review reference — tests don't auto-apply the patch.
The runtime block is optional. Fixtures without it only run the static unit checks (is-generated, inject, wrap, csp-detect). Fixtures with it additionally run the E2E suite in tests/live-e2e.test.mjs (bun run test:live-e2e), which:
- Stages the fixture into a tmp repo.
- Runs
runtime.installto install real deps. - Starts
live-server.mjs --backgroundand runslive-inject.mjs --portagainst it. - Spawns
runtime.devCommandand scrapes the port from stdout usingruntime.readyPattern(the first capture group must be the port). - Opens Playwright Chromium at the dev URL and asserts
window.__IMPECCABLE_LIVE_INIT__ === true(the browser-side handshake oracle) withinruntime.readyTimeoutMs. - Runs a Steer smoke step (unless
runtime.steer === false): submit a message in the global Steer bar, wait for the fake agent to replysteer_done, assert the bar unlocks and adata-impeccable-steermarker lands in source + DOM. Then continues with pick → Go → cycle → accept. - Tears everything down (Playwright close, dev server SIGTERM, live-server stop, tmp rm).
runtime.appDir
Optional, defaults to .. Set it when the served app is not the repo root, the shape live mode has to resolve on its own (a CLI package at the root with the site in website/, for example). With appDir set, the harness:
- stages
files/and runsgit initat the tmp root, as always; - writes
.impeccable/live/config.jsonunder<tmp>/<appDir>/, and treats every fixture-relative path infixture.json(steer.sourceFile, manual-editexpectedSourceFile, and so on) as relative to that app dir; - runs
runtime.installandruntime.devCommandwith the app dir as cwd; - boots through
live.mjsfrom the tmp root instead of callinglive-server.mjsandlive-inject.mjsdirectly, so the run exercises root resolution (skill/scripts/live/roots.mjs) rather than assuming it. The parsedlive.mjspayload is exposed assession.liveBoot, andtests/live-e2e.test.mjsasserts onroots.appRoot,roots.contextRoot, the persistedroots.json, and the repo-root pointer.
The session object carries both paths: session.tmp is the repo root (use it for git and for artifact capture) and session.appRoot is the app. They are the same directory for every fixture without appDir.
Picking, cycling, and the render proof
pickSelector names the element the run picks. The picker resolves whatever is
under the cursor, so a container whose centre is covered by a child can never
be picked: add pickPosition ({x, y} in px from the element's top-left) to
aim at a point the container owns, such as its own padding.
variantSequence (default [2]) is the order the run cycles through; the last
entry is the variant it accepts. Every variant the run lands on gets a computed
font-weight assertion in fake-agent mode, because the fake agent renders each
variant at a distinct weight (FAKE_VARIANT_FONT_WEIGHTS in
tests/live-e2e/agent.mjs: 300 / 900 / 600). That turns "variant N is visible"
from a bar-label claim into a render fact, so a sequence like [3, 1, 2] proves
all three variants really render.
acceptedSourcePattern overrides the default post-accept source check (an <h1 class="hero-title">), and assertSourceContains lists strings that must
survive the whole wrap → accept → carbonize cycle. On Svelte component previews
that is how a fixture proves control flow was not flattened: list the {#each}
header and the per-item expressions.
stateProbe asserts page state is not lost. textSelector / expectedText
check rendered state; windowProperty with expectedWindowValue and
expectWindowUnchanged check a counter the app bumps on mount, so a scaffold
that silently remounted the page fails. It runs after preActions, after the
variants land, and (outside component previews) after accept.
Extra scenarios
Beyond the core cycle, a fixture opts into scenarios by declaring their config.
Each one is also gated by IMPECCABLE_E2E_SCENARIOS:
| Scenario name | Enabled by | What it proves |
|---|---|---|
params |
runtime.paramsScenario |
Dial a range and a steps knob in the real Tune popover, accept, and assert the chosen values are baked into source as literals with the unchosen branch dropped and no data-p-* / var(--p-*) left behind. |
mount-failure |
runtime.componentFailureScenarios |
Corrupt the published r<N>/v<variant> revision file, step onto it, and assert the persistent mount-error card appears, the session survives (bar + localStorage intact), and a variant_mount_failed event lands in the session journal. Then restore, Retry, and reach the variant again. |
republish |
runtime.componentFailureScenarios |
Re-author every variant and reply done again; the browser must mount the new content, which is what the server's revision-dir bump exists to guarantee. |
storage-loss |
runtime.componentFailureScenarios (unless storageLoss: false) |
Clear localStorage, reload, and assert the comparison comes back from the server's durable session record alone. |
componentFailureScenarios.variant picks which variant to break or observe.
Set storageLoss: false for fixtures whose picked element only exists after
preActions: the reload discards that state, and re-creating it races the
preview mount.
These scenarios assert on deterministic content, so they skip under
IMPECCABLE_E2E_AGENT=llm.
One gotcha: tests/framework-fixtures.test.mjs stages the same fixture flat and writes the live config at the tmp root, so config.files has to resolve from both the repo root and the app root. A glob ("**/index.html") satisfies both; a literal "index.html" only works from the app root and makes the static sweep report file_not_found.
Useful runtime E2E filters:
IMPECCABLE_E2E_ONLY=<fixture>[,<fixture>]scopes the run to selected fixture names.IMPECCABLE_E2E_SCENARIOS=coreruns only the main click → Go → cycle → accept path; omit it or useallto include manual edit, annotation, exit, params, and the component failure-injection probes. Names:core,manual,annotations,exit,missed-done,params,mount-failure,republish,storage-loss.IMPECCABLE_E2E_TEST_TIMEOUT_MS,IMPECCABLE_E2E_INSTALL_TIMEOUT_MS, andIMPECCABLE_E2E_DEV_READY_TIMEOUT_MStighten CI smoke timeouts without changing fixture metadata.
Optional runtime.steer fields:
"steer": {
"message": "steer-e2e mark hero",
"sourceFile": "src/routes/About.jsx",
"expectSelector": "h1.hero-title[data-impeccable-steer=\"e2e\"]",
"expectSourceContains": "data-impeccable-steer=\"e2e\"",
"preActions": [{ "type": "click", "selector": "[data-testid='nav-about']" }]
}
When preActions is omitted, steer smoke inherits runtime.preActions to reveal hidden heroes before the DOM check. Source is asserted first; a reload + retry covers HMR lag. Set "steer": false to skip, or "expectDom": false for source-only verification.
Current fixtures
| Fixture | Shape |
|---|---|
vite-react/ |
Tracked index.html shell + src/App.jsx. Inject into the shell. |
nextjs-app/ |
app/layout.tsx as JSX inject target (commentSyntax jsx). |
astro/ |
src/layouts/Layout.astro as inject target. HTML comments. |
sveltekit/ |
src/app.html shell + src/routes/+page.svelte. |
vite8-sveltekit-stateful/ |
Svelte 5 route with $state, an {#if} branch, and an {#each} list. Picks the list container, so the component-preview scaffold has to carry the loop across as one collection prop and hydrate its items from the live DOM. Also carries the params, failure-injection, and state-preservation probes. |
nuxt-vite7/ |
Nuxt 4 app/ structure + Vue 3 SFC. Live loads through a generated dev-only client plugin. |
tanstack-router-vite/ |
Vite + TanStack Router (code-based SPA). Tracked index.html shell inject (the baseline Vite path, no adapter). |
tanstack-start/ |
Vite + TanStack Start (SSR). No static index.html; Live patches the __root.tsx document to mount a generated dev-only React component that loads the bundle. |
multipage-with-generator/ |
src/ tracked, dist/ gitignored. Exercises the is-generated guard and element_not_in_source fallback. |
nextjs-turborepo/ |
Monorepo with shared CSP helper (createBaseNextConfig). CSP shape append-arrays. |
nextjs-inline-csp/ |
App-level next.config.js with a literal CSP string. CSP shape append-string. |
sveltekit-csp/ |
SvelteKit kit.csp.directives in svelte.config.js. CSP shape append-arrays. |
nuxt-csp/ |
Nuxt routeRules with literal CSP header in nuxt.config.ts. CSP shape append-string. |
monorepo-nested-vite/ |
Repo root is a CLI package with no dev config and no workspaces; the served Vite + React app lives in website/. Exercises runtime.appDir and live-mode root resolution. |
Add new fixtures by cloning a directory, swapping files, and updating fixture.json. A fixture with a runtime block also needs its name added to the live-e2e matrices in .github/workflows/ci.yml: the live-e2e-full group list, and one live-e2e-smoke group when it should run on every PR. Keep the groups roughly the same size, since they run in parallel and the job times out at 15 minutes.