# Live browser iteration

> Start live mode, poll contract, generate/accept/discard/steer/manual-edit events, carbonize cleanup, roots resolution, and web-only platform constraints.

- Repository: pbakaus/impeccable
- GitHub: https://github.com/pbakaus/impeccable
- Human docs: https://grok-wiki.com/public/docs/pbakaus-impeccable-adadc04d8de4
- Complete Markdown: https://grok-wiki.com/public/docs/pbakaus-impeccable-adadc04d8de4/llms-full.txt

## Source Files

- `skill/reference/live.md`
- `skill/reference/live-setup.md`
- `skill/scripts/live.mjs`
- `skill/scripts/live-server.mjs`
- `skill/scripts/live/roots.mjs`
- `skill/scripts/live-wrap.mjs`
- `skill/scripts/live-accept.mjs`
- `docs/adr-live-variant-mode.md`

---

---
title: Live browser iteration
description: Start live mode, poll contract, generate/accept/discard/steer/manual-edit events, carbonize cleanup, roots resolution, and web-only platform constraints.
---

Live mode lets you pick an element in a running web page, choose a design action, and review AI-generated HTML+CSS variants hot-swapped through the app’s own HMR (or a no-HMR source fallback). Variants are written into real source files so Accept is a source mutation, not a DOM patch.

## What you get

Three parties share one session:

| Role | Responsibility |
|------|----------------|
| **Browser overlay** (`live.js`) | Element picker, action bar, variant cycler, Steer/Apply controls, SSE + POST to the helper |
| **Live helper server** (`live-server.mjs`) | Localhost HTTP on `127.0.0.1` (port from **8400** upward), `/poll`, `/events`, `/source`, session journal |
| **Agent** | Boot via `live.mjs`, long-poll via `live-poll.mjs`, wrap/write variants, carbonize after Accept |

Design context for generation uses this precedence: **DESIGN.md** for visual decisions, **PRODUCT.md** for product and voice, surface brief for that surface’s strategy. Missing DESIGN.md does not mean “no identity”: extract tokens and sibling rhetoric from the live element.

## Web-only constraint

Live mode and the HTML detector overlay are **web-only**. When project platform is `ios`, `android`, or `adaptive`, routing must not lead with live: the browser inject path and HTML rule engine do not apply to native app sources. Use web projects (or web surfaces of a multi-platform product) only.

## Prerequisites

- A runnable web app with a dev server that supports HMR (Vite, Next, Nuxt, SvelteKit, Astro, TanStack, plain HTML, etc.), **or** a static HTML page open in the browser.
- `PRODUCT.md` and `DESIGN.md` present (boot fails with `context_missing` and points at `init` / `document` otherwise).
- One-time `.impeccable/live/config.json` (and CSP consent when needed).
- Prefer probing the app’s default URL before spawning a second dev server if the default port is busy.

Do not confuse **`serverPort`** with the app URL. The helper serves `/live.js` and `/poll`; open the origin that serves a configured page file.

## Architecture

```mermaid
sequenceDiagram
  participant User
  participant Browser as Browser overlay
  participant Server as live-server
  participant Agent as Agent + live-poll

  User->>Browser: Pick element, action, Go
  Browser->>Server: POST /events generate
  Agent->>Server: GET /poll (long-poll)
  Server-->>Agent: generate event + scaffold
  Agent->>Agent: wrap + write variants
  Agent->>Server: POST /poll done --file
  Server-->>Browser: SSE done
  Browser->>Browser: HMR or /source inject
  User->>Browser: Accept / Discard / Steer
  Browser->>Server: POST /events
  Server-->>Agent: next poll event
```

**Source modification, not DOM-only patching.** Accept keeps the winning markup in source. SSE (server→browser) + fetch POST (browser→server) avoid a WebSocket dependency. Agent traffic is HTTP long-poll so any harness that can run a shell command can participate.

Helper endpoints (selected):

| Path | Role |
|------|------|
| `GET /live.js` | Injected browser script (token embedded) |
| `GET/POST /events` | SSE stream + browser events |
| `GET/POST /poll` | Agent long-poll and replies |
| `GET /source` | Raw file for no-HMR fallback |
| `/manual-edit-stash`, `/manual-edit-commit`, `/manual-edit-discard` | Staged browser copy edits |
| `GET /status`, `GET /health` | Recovery / health |
| `GET /stop` | Graceful shutdown |

Session state lives under `<appRoot>/.impeccable/live/` (`server.json`, `roots.json`, `sessions/`, inject journal). The append-only journal is canonical for recovery after helper restarts or chat interruptions.

## Roots resolution

Every live CLI calls `enterLiveRoot()` so ambient `cwd` cannot fork session state.

| Root | Meaning |
|------|---------|
| `appRoot` | Dev-served app; inject targets, session state, preview modules |
| `repoRoot` | Git boundary (or `appRoot` outside git) |
| `contextRoot` | Nearest dir up to `repoRoot` with PRODUCT/DESIGN |
| `sessionRoot` | `<appRoot>/.impeccable/live` |

`appRoot` is detected from dev-server config markers (`vite.config.*`, `next.config.*`, `svelte.config.*`, `astro.config.*`, `nuxt.config.*`, …) or an existing live config, not monorepo branding. Nested `website/` with Vite wins over a root that only has `package.json`. Manifest is written to `<appRoot>/.impeccable/live/roots.json`; when `repoRoot ≠ appRoot`, a pointer at `<repoRoot>/.impeccable/live/app-root.json` helps helpers started from elsewhere. Multi-app repos: pass `--target <path>` or run from the child app; ambiguous multi-app state is warned on stderr.

**Svelte preview modules** are published under `node_modules/.impeccable-live/` (not under `.impeccable/`) so Vite/SvelteKit `server.fs.allow` can load them.

## First-time setup

Required when boot returns `config_missing` / `config_invalid`, when `configDrift` needs a decision, or when `cspChecked` is absent.

### Live config

Create `.impeccable/live/config.json` (path reported by boot):

```json
{
  "files": ["index.html"],
  "exclude": [],
  "insertBefore": "</body>",
  "commentSyntax": "html",
  "cspChecked": true
}
```

| Field | Role |
|-------|------|
| `files` | HTML/templates the browser actually loads (paths or globs), not necessarily “tracked source” |
| `exclude` | Optional globs skipped after expand |
| `insertBefore` / `insertAfter` | Injection anchor |
| `commentSyntax` | `html` or `jsx` |
| `cspChecked` | CSP consent already handled |

Hard excludes (cannot override): `**/node_modules/**`, `**/.git/**`.

| Framework | Typical `files` | `commentSyntax` |
|-----------|-----------------|-----------------|
| Vite / plain SPA | `index.html` | `html` |
| Next App Router | `app/layout.tsx` | `jsx` |
| Next Pages | `pages/_document.tsx` | `jsx` |
| Nuxt | `app.vue` | `html` |
| SvelteKit | `src/app.html` | `html` |
| TanStack Start | `src/routes/__root.tsx` | `jsx` (`insertBefore`: `<Scripts`) |
| Multi-page | e.g. `public/**/*.html` | `html` |

SvelteKit, Nuxt, and TanStack Start use dedicated inject adapters (dev-only root component / client plugin / generated root component). Inject journal: `.impeccable/live/inject-journal.json`.

**Config drift:** boot may report `configDrift.orphans` (HTML under `public/`, `src/`, `app/`, `pages/` not covered by `files`). Tell the user once; never auto-edit config.

### CSP (first time)

Run `detect-csp.mjs`. If shape is `null`, set `cspChecked: true`. Auto-patchable shapes (`append-arrays`, `append-string`) add a **dev-only** `http://localhost:8400` allowance to `script-src` and `connect-src` after explicit consent. Middleware/meta-tag shapes need a manual allowance, then still mark `cspChecked: true`. On “no”, mark checked anyway and expect live to fail until the user allows the origin.

## Start a session

```bash
node <scripts_path>/live.mjs
# monorepo / nested app:
node <scripts_path>/live.mjs --target <app-or-file-path>
```

Boot order: resolve roots → require PRODUCT/DESIGN → check config → start or reuse helper → inject script → optional drift scan → print one JSON blob.

Success shape (fields of interest):

| Field | Meaning |
|-------|---------|
| `ok` | `true` when ready to poll |
| `serverPort` / `serverToken` | Helper only |
| `pageFiles` | Resolved inject targets |
| `projectRoot` / `roots` | App and related roots |
| `product` / `design` / `surfaceBrief` | Inlined context |
| `configDrift` | Orphans advisory or `null` |
| `_instructions` | Authoritative next steps for this boot |

Then open the **app** URL for a `pageFile`, and enter the poll loop.

### Poll loop

```text
LOOP:
  node <scripts_path>/live-poll.mjs    # default long timeout; do not use short --timeout=
  dispatch on event.type
  reply when required
  LOOP until exit
```

Default event lease is long (~600s). Do not pass a short `--timeout=`. The global bar’s Impeccable mark dims with a pulsing amber dot when nothing is polling `/poll`.

Harness policy:

| Harness | Poll style |
|---------|------------|
| Claude Code | Background poll (harness notifies) |
| Cursor | One-shot background poll + restart; **not** `--stream` |
| Codex | One-shot foreground poll; keep servicing the exec session |
| Others | One-shot foreground unless incremental stdout is reliable |

Every tool output may include `_instructions` with concrete ids and paths. When it conflicts with static docs, **`_instructions` wins**.

Events that need an agent reply: `generate`, `steer`, `manual_edit_apply`, `carbonize_cleanup`, `variant_mount_failed`. Accept/discard are mostly handled by the poll script itself.

## Events

Browser/client types (wire vocabulary) include: `generate`, `accept`, `discard`, `steer`, `prefetch`, `manual_edits`, `variant_mounted`, `variant_mount_failed`, `exit`, plus progress types (`agent_phase`, `checkpoint`, …). Agent-facing poll types also include `timeout`, `manual_edit_apply`, and completion payloads.

### `generate`

Replace mode (default): element + action (`impeccable` freeform or a named action such as `bolder`, `polish`, `typeset`, …) + optional annotations (`screenshotPath`, `comments`, `strokes`).

Insert mode (`mode: "insert"`): net-new content at an anchor; requires freeform prompt or annotations; no `action`.

Agent path:

1. Reuse `event.scaffold` when present (do not re-run wrap).
2. Else run `live-wrap.mjs` / `live-insert.mjs` with separate `--element-id`, `--classes`, `--tag`, `--text` flags (`--text` disambiguates siblings).
3. Plan **three** variants within identity (default) or departure only on explicit redesign language.
4. Write complete HTML replacements + colocated CSS per `cssAuthoring` / `styleMode` (atomic single edit preferred).
5. Optional 0–4 parameters per variant (`range` / `steps` / `toggle`).
6. Reply: `live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH`.

On generation failure after the UI shows GENERATING: `--reply EVENT_ID error "Short reason"`. Never use `live-accept --discard` to abort generation (browser bar never clears).

**Svelte component path:** preview under `node_modules/.impeccable-live/<id>/`; edit `v1.svelte`… in place; params in `params.json`; accept merges mechanically into the real route.

**Fallback (`fallback: "agent-driven"`):** wrap refused generated/untracked files. Preview in the served file; on Accept write true source yourself; strip the temporary wrapper.

### `accept`

Poll already ran `live-accept.mjs` and may attach `_acceptResult` and `_completionAck`.

| Result | Agent action |
|--------|----------------|
| `handled: true, carbonize: false` | Done; poll again |
| `handled: true, carbonize: true` | Required carbonize cleanup, then `live-complete.mjs --id EVENT_ID` |
| `handled: false, mode: "fallback"` | Persist to true source; remove temp wrapper |
| `handled: false, mode: "error"` | Do not hand-edit; retry lock or check `live-status` (`source_locked`, `accept_receipt_conflict`) |

Accept refuses generated files. Param values arrive as sibling comments; carbonize bakes them.

### Carbonize cleanup

When Accept stitches the winner with helper markers so the browser never flashes empty, the stitch is temporary. Before the next poll:

1. Find `impeccable-carbonize-start/end` and optional `impeccable-param-values`.
2. Move CSS into the project’s real stylesheet.
3. Bake params: keep chosen `steps`/`toggle` branches; substitute `range` values; collapse `@scope` / `data-p-*` to semantic rules.
4. Unwrap variant/carbonize wrappers and `data-impeccable-*` attrs.
5. Delete inline style, markers, leftover non-accepted scopes.

Then:

```bash
node <scripts_path>/live-complete.mjs --id SESSION_ID
```

Completion refuses with `error: "source_dirty"` while markers, `data-p-*`, or unbaked `--p-*` remain (`--force` only for false positives). Verify `phase: "completed"` before polling again.

### `discard`

Poll restores original and acknowledges. If `_completionAck.ok !== true`, run `live-complete.mjs --id EVENT_ID --discarded`, then poll.

### `steer`

Page-level direction from the global bar (no element, no variants). Edit or answer, then `--reply EVENT_ID steer_done ["toast"]` or `error`. No pickup ack.

### `prefetch`

Speculative route pre-read on first selection. No reply; poll again after reading.

### `manual_edit_apply`

User already clicked Apply on staged copy edits. Do not re-ask or redirect to Go. Delegate to the manual-edit applier agent when available (it must not poll/reply). Reply once with structured JSON:

```bash
node <scripts_path>/live-poll.mjs --reply EVENT_ID done --data \
  '{"status":"done","appliedEntryIds":["8hexid"],"failed":[],"files":["src/page.html"],"notes":[]}'
```

Use `status: "partial"` or `"error"` with `failed[]` when needed. If `repair` is set, fix current source; do not roll back yourself.

### `variant_mount_failed`

Published variant failed to render. Fix sources, `--reply EVENT_ID done --file <manifest-or-source>`; browser retries.

### `timeout` / `exit`

`timeout` → poll again. `exit` → cleanup (below). Closing the tab drops SSE; poll returns `exit` after a short grace period (~8s).

## Recovery

```bash
node <scripts_path>/live-status.mjs
node <scripts_path>/live-resume.mjs --id SESSION_ID
node <scripts_path>/live-complete.mjs --id SESSION_ID
```

Journal under `.impeccable/live/sessions/` is durable. After helper restart, start server again and poll; unacknowledged work is requeued. Fall back to non-live direct edits only when resume reports **no** active session.

## Exit and cleanup

```bash
node <scripts_path>/live-server.mjs stop
# keep inject for a quick restart:
node <scripts_path>/live-server.mjs stop --keep-inject
```

Stop removes the injected script (unless `--keep-inject`). Config persists. Sweep leftover `impeccable-variants-start` and `impeccable-carbonize-start` blocks from source.

## Troubleshooting

| Symptom | Check |
|---------|--------|
| Boot `config_missing` | Create config per setup; re-run `live.mjs` |
| Overlay never loads | CSP blocking `localhost:8400`; clear `cspChecked` and re-run CSP flow |
| Amber dimmed mark | No active poll; restart `live-poll.mjs` |
| Accept leaves markers | Finish carbonize; `live-complete` until clean |
| Wrong monorepo app | Rerun with `--target` or from child cwd |
| `element_ambiguous` / generated file | Agent-driven fallback; never accept into generated output |
| Generation stuck on dots | `--reply EVENT_ID error "…"` (not accept discard) |
| Multi-app warning | Explicit `--target` to the intended `appRoot` |

## Related pages

<CardGroup cols={2}>
  <Card title="Modes and platform" href="/modes-and-platform">
    Web-only live/detect boundaries and platform defaults.
  </Card>
  <Card title="Live framework recipes" href="/live-framework-recipes">
    Copy-paste live config shapes for Vite, Next, Nuxt, SvelteKit, TanStack, Astro, multipage, CSP fixtures.
  </Card>
  <Card title="Project artifacts" href="/project-artifacts">
    PRODUCT.md, DESIGN.md, surface briefs, and `.impeccable/` layout.
  </Card>
  <Card title="Configuration reference" href="/configuration-reference">
    Live config.json fields and path excludes.
  </Card>
  <Card title="Plugin agents" href="/plugin-agents">
    Manual-edit applier and related bundled agents.
  </Card>
  <Card title="Build and test" href="/build-and-test">
    Opt-in live-e2e suite and framework fixtures.
  </Card>
</CardGroup>
