diff --git a/tests/oracle/README.md b/tests/oracle/README.md
index 3b15d539b..d4c32458c 100644
--- a/tests/oracle/README.md
+++ b/tests/oracle/README.md
@@ -55,3 +55,35 @@ Not covered on purpose: `palette` with no `--id` / `--from` / env seed (random),
`concept-seed` against the live roll API, `generate-image` real mode,
`serve-question --start` / blocking mode (opens a browser and binds a port),
and unhandled-exception paths whose stack traces carry Node line numbers.
+
+## Live-mode cases (`cases/live-*.mjs`, workspaces `live-*`)
+
+Helpers live in `live-helpers.mjs` (staged journals, buffers, wrapped source
+files with the fake-agent variant block, a `.git` FILE pointing at a non-repo
+gitdir so roots resolution sees a git boundary while `git check-ignore` exits
+128 everywhere and the ignore block lands in the snapshotable
+`.gitfake/info/exclude`). Svelte component preview cases symlink this repo's
+`node_modules/svelte` into the staged app, exactly like the unit tests.
+
+Harness additions made for live:
+
+- `steps[]` entries may carry their own `setup(ws)` (run right before that
+ step) and `daemon: true` with `readyFile` / `readyTimeoutMs`: the verb is
+ spawned detached, the harness waits for the ready file, later steps run
+ against it, and teardown SIGTERMs (then SIGKILLs) it. Its stdout/stderr land
+ in the golden as `daemon: [{stdout, stderr}]`.
+- `normalize: [[regexSource, flags, replacement], ...]` on a case applies extra
+ masks to that case only. Live uses it for the dynamic helper port
+ (`localhost:`, `"port": `), lease and phase stamps (``),
+ and float durations (``).
+- Global masks added: `"pid": ` / `(pid )` and UUID tokens ``.
+- `snapshotFiles` walks `node_modules/.impeccable-live` (the Svelte preview
+ tree) and nothing else under `node_modules`.
+
+Deliberately not covered here (rely on `tests/live-e2e`): the browser
+handshake and `/live.js` bundle, SSE, generate/accept round-trips through a
+real browser, `variant_mount_failed` republish, manual-edit chat routing and
+the codex/claude subprocess providers, Svelte revision-dir publishing, and
+`live.mjs`'s dev-server-dependent flows. Lock-file names hash the absolute
+source path, so lock cases do not snapshot `.impeccable/live/locks/`.
+`live-poll-*-connection-refused` assumes nothing listens on 127.0.0.1:65531.
diff --git a/tests/oracle/cases/live-accept.mjs b/tests/oracle/cases/live-accept.mjs
new file mode 100644
index 000000000..a9af86b89
--- /dev/null
+++ b/tests/oracle/cases/live-accept.mjs
@@ -0,0 +1,165 @@
+/**
+ * `live-accept` corpus: argument paths, HTML and JSX accept/discard
+ * (carbonize output, param baking inputs), receipts, lock contention,
+ * generated-file fallback, buffer scrub, and the Svelte component path
+ * (wrap with the linked compiler, then accept/discard the stub variant).
+ */
+import fs from 'node:fs';
+import path from 'node:path';
+import {
+ LIVE_FILES, linkSvelte, lockPathFor, stageWrappedHtml, stageWrappedJsx, write, writeBuffer,
+} from '../live-helpers.mjs';
+
+const F = LIVE_FILES;
+const HTML = [...F, 'index.html'];
+const JSX = [...F, 'src/**'];
+const A = (id, workspace, args, extra = {}) => ({ id, verb: 'live-accept', workspace, args, files: [...F, ...(extra.snap || [])], ...extra });
+const ID = 'ab12cd34';
+
+export default [
+ A('live-accept-help', 'live-html', ['--help']),
+ A('live-accept-missing-id', 'live-html', ['--variant', '1']),
+ A('live-accept-invalid-id', 'live-html', ['--id', '../etc', '--variant', '1']),
+ A('live-accept-need-mode', 'live-html', ['--id', ID]),
+ A('live-accept-invalid-variant', 'live-html', ['--id', ID, '--variant', '1a']),
+ A('live-accept-invalid-variant-regex', 'live-html', ['--id', ID, '--variant', '.*']),
+ A('live-accept-markers-not-found', 'live-html', ['--id', 'nothere1', '--variant', '1']),
+ A('live-accept-target-missing-value', 'live-html', ['--id', ID, '--variant', '1', '--target']),
+
+ // --- HTML source-preview accept / discard ---
+ A('live-accept-html-variant-1-range', 'live-html', ['--id', ID, '--variant', '1', '--param-values', '{"lightness":0.65}'], { snap: ['index.html'], setup: stageWrappedHtml }),
+ A('live-accept-html-variant-2-steps', 'live-html', ['--id', ID, '--variant', '2', '--param-values', '{"face":"serif"}'], { snap: ['index.html'], setup: stageWrappedHtml }),
+ A('live-accept-html-variant-3-toggle', 'live-html', ['--id', ID, '--variant', '3', '--param-values', '{"italic":true}'], { snap: ['index.html'], setup: stageWrappedHtml }),
+ A('live-accept-html-no-params', 'live-html', ['--id', ID, '--variant', '2'], { snap: ['index.html'], setup: stageWrappedHtml }),
+ A('live-accept-html-malformed-params', 'live-html', ['--id', ID, '--variant', '1', '--param-values', '{bad'], { snap: ['index.html'], setup: stageWrappedHtml }),
+ A('live-accept-html-variant-missing', 'live-html', ['--id', ID, '--variant', '9'], { snap: ['index.html'], setup: stageWrappedHtml }),
+ A('live-accept-html-discard', 'live-html', ['--id', ID, '--discard'], { snap: ['index.html'], setup: stageWrappedHtml }),
+ A('live-accept-html-receipt-replay', 'live-html', ['--id', ID, '--variant', '1'], { snap: ['index.html'], setup: stageWrappedHtml, steps: [{}, {}] }),
+ A('live-accept-html-receipt-conflict', 'live-html', [], { snap: ['index.html'], setup: stageWrappedHtml, steps: [{ args: ['--id', ID, '--variant', '1'] }, { args: ['--id', ID, '--variant', '2'] }, { args: ['--id', ID, '--discard'] }] }),
+ A('live-accept-html-discard-then-accept', 'live-html', [], { snap: ['index.html'], setup: stageWrappedHtml, steps: [{ args: ['--id', ID, '--discard'] }, { args: ['--id', ID, '--variant', '1'] }] }),
+ A('live-accept-html-no-css-block', 'live-html', ['--id', ID, '--variant', '2'], {
+ snap: ['index.html'],
+ setup: (ws) => {
+ stageWrappedHtml(ws);
+ const p = path.join(ws, 'index.html');
+ fs.writeFileSync(p, fs.readFileSync(p, 'utf8').replace(/ {8}\n');
+ fs.writeFileSync(path.join(dir, 'params.json'), JSON.stringify({ 2: [
+ { id: 'lead', kind: 'range', min: 1, max: 2, step: 0.1, default: 1.2, label: 'Lead' },
+ { id: 'density', kind: 'steps', default: 'airy', label: 'Density', options: [{ value: 'airy', label: 'Airy' }, { value: 'snug', label: 'Snug' }] },
+ ] }, null, 2));
+ },
+ },
+ ],
+ },
+ {
+ id: 'live-accept-svelte-component-discard', workspace: 'live-sveltekit', files: [...F, 'src/**'], setup: linkSvelte,
+ steps: [
+ { verb: 'live-wrap', args: ['--id', ID, '--classes', 'expense-list', '--tag', 'ul'] },
+ { verb: 'live-accept', args: ['--id', ID, '--discard'] },
+ { verb: 'live-accept', args: ['--id', ID, '--discard'] },
+ ],
+ },
+ {
+ id: 'live-accept-svelte-component-variant-missing', workspace: 'live-sveltekit', files: [...F, 'src/**'], setup: linkSvelte,
+ steps: [
+ { verb: 'live-wrap', args: ['--id', ID, '--classes', 'hero-title', '--tag', 'h1'] },
+ { verb: 'live-accept', args: ['--id', ID, '--variant', '7'] },
+ ],
+ },
+ {
+ id: 'live-accept-svelte-insert-empty-variant', workspace: 'live-sveltekit', files: [...F, 'src/**'],
+ steps: [
+ { verb: 'live-insert', args: ['--id', ID, '--position', 'after', '--element-id', 'features'] },
+ { verb: 'live-accept', args: ['--id', ID, '--variant', '1'] },
+ ],
+ },
+ {
+ id: 'live-accept-svelte-fallback-source-wrapper', workspace: 'live-sveltekit', files: [...F, 'src/**'],
+ // No compiler: wrap wrote a plain source wrapper into the .svelte route.
+ steps: [
+ { verb: 'live-wrap', args: ['--id', ID, '--classes', 'hero-title', '--tag', 'h1'] },
+ { verb: 'live-accept', args: ['--id', ID, '--discard'] },
+ ],
+ },
+];
diff --git a/tests/oracle/cases/live-daemon.mjs b/tests/oracle/cases/live-daemon.mjs
new file mode 100644
index 000000000..0ef808e7f
--- /dev/null
+++ b/tests/oracle/cases/live-daemon.mjs
@@ -0,0 +1,45 @@
+/**
+ * One end-to-end helper-server flow driven through the daemon step support
+ * in lib.mjs: start `live-server` detached, then status / poll / reply /
+ * complete / stop against it. Everything run-dependent (port, token, pids,
+ * lease and phase timestamps, durations) is masked per-case.
+ */
+import { LIVE_FILES, writeJournal } from '../live-helpers.mjs';
+
+const ID = 'ab12cd34';
+const GEN = { type: 'generate', action: 'bolder', count: 3, pageUrl: '/', element: { tagName: 'h1', id: 'hero', classes: ['hero-title'], textContent: 'Oracle Fixture', outerHTML: 'Oracle Fixture
' }, clientSentAt: 1754042400000 };
+const NORM = [
+ ['localhost:\\d{4,5}', 'g', 'localhost:'],
+ ['"(port|serverPort)":(\\s*)\\d{4,5}', 'g', '"$1":$2'],
+ ['Stopped live server on port \\d+\\.', 'g', 'Stopped live server on port .'],
+ ['already running on port \\d+ ', 'g', 'already running on port '],
+ ['"(leaseUntil|generationReadyAt|lastPollAt|generationCompletedAt|generationCanceledAt|at|leasedAt|clientSentAt)":(\\s*)\\d{10,}', 'g', '"$1":$2'],
+ ['"(durationMs|scaffoldDurationMs)":(\\s*)\\d+(?:\\.\\d+)?', 'g', '"$1":$2'],
+ // The journal embeds the float duration above, so its byte length moves too.
+ ['"__journalBytes":(\\s*)\\d+', 'g', '"__journalBytes":$1'],
+];
+
+export default [
+ {
+ id: 'live-daemon-server-status-poll-complete', workspace: 'live-html', files: [...LIVE_FILES, 'index.html'], normalize: NORM,
+ // A durable session with a pending generate: the server restores it into
+ // its queue on start, so the first poll leases it (running the deferred
+ // wrap preflight against index.html) instead of timing out.
+ setup: (ws) => writeJournal(ws, ID, [GEN]),
+ steps: [
+ { verb: 'live-server', daemon: true, readyFile: '.impeccable/live/server.json', readyTimeoutMs: 15000 },
+ { verb: 'live-server', args: [] },
+ { verb: 'live-status', args: [] },
+ { verb: 'live-poll', args: ['--timeout=8000'] },
+ { verb: 'live-poll', args: ['--reply', ID, 'done', '--file', 'index.html'] },
+ { verb: 'live-poll', args: ['--timeout=300'] },
+ { verb: 'live-poll', args: ['--reply', 'zz99zz99', 'done'] },
+ { verb: 'live-poll', args: ['--reply', ID, 'steer_done'] },
+ { verb: 'live-resume', args: [] },
+ { verb: 'live-complete', args: ['--id', ID] },
+ { verb: 'live-status', args: [] },
+ { verb: 'live-server', args: ['stop'] },
+ { verb: 'live-status', args: [] },
+ ],
+ },
+];
diff --git a/tests/oracle/cases/live-manual.mjs b/tests/oracle/cases/live-manual.mjs
new file mode 100644
index 000000000..1896067b5
--- /dev/null
+++ b/tests/oracle/cases/live-manual.mjs
@@ -0,0 +1,80 @@
+/**
+ * Manual copy-edit helpers: `live-discard-manual-edits`,
+ * `live-commit-manual-edits` (mock provider only: no codex/claude spawn),
+ * driven from a staged pending-manual-edits.json buffer.
+ */
+import { LIVE_FILES, write, writeBuffer } from '../live-helpers.mjs';
+
+const F = [...LIVE_FILES, 'index.html', 'src/**', 'public/**'];
+const entry = (id, pageUrl, ops) => ({ id, pageUrl, element: { tagName: 'h1', classes: ['hero-title'] }, ops, stagedAt: '2026-08-01T00:00:00.000Z' });
+const twoPages = (ws) => writeBuffer(ws, [
+ entry('e1', '/', [{ ref: 'main>h1', tag: 'h1', classes: ['hero-title'], originalText: 'Oracle Fixture', newText: 'Oracle Fixture, renamed', sourceHint: { file: 'index.html', line: 13, column: 7 } }]),
+ entry('e2', '/', [{ ref: 'main>p', tag: 'p', originalText: 'Minimal static page for oracle live-mode goldens.', newText: 'A tighter hook.' }, { ref: 'aside>p', tag: 'p', originalText: 'Nested content.', newText: 'Nested copy.' }]),
+ entry('e3', '/docs/guide', [{ ref: 'article>p', tag: 'p', originalText: 'A second page covered by the config glob.', newText: 'Second page lead.' }]),
+]);
+const D = (id, args, setup, extra = {}) => ({ id, verb: 'live-discard-manual-edits', workspace: 'live-html', args, setup, files: F, ...extra });
+const C = (id, args, setup, extra = {}) => ({ id, verb: 'live-commit-manual-edits', workspace: 'live-html', args, setup, files: F, ...extra });
+const MOCK = { IMPECCABLE_LIVE_COPY_AGENT: 'mock' };
+
+export default [
+ D('live-discard-help', ['--help'], null),
+ D('live-discard-empty', [], null),
+ D('live-discard-all', [], twoPages),
+ D('live-discard-page', ['--page-url=/'], twoPages),
+ D('live-discard-page-no-match', ['--page-url=/nope'], twoPages),
+ D('live-discard-page-bare-flag', ['--page-url'], twoPages),
+ D('live-discard-twice', [], twoPages, { steps: [{}, {}] }),
+ D('live-discard-invalid-buffer', [], (ws) => write(ws, '.impeccable/live/pending-manual-edits.json', '{ nope')),
+
+ C('live-commit-help', ['--help'], null),
+ C('live-commit-no-pending', [], null, { env: MOCK }),
+ C('live-commit-no-pending-page', ['--page-url=/'], null, { env: MOCK }),
+ C('live-commit-invalid-buffer', [], (ws) => write(ws, '.impeccable/live/pending-manual-edits.json', '{ nope'), { env: MOCK }),
+ C('live-commit-mock-default-result', ['--provider=mock'], twoPages, {
+ // Mock reports every entry applied but touched no files: source
+ // verification finds the new text nowhere and refuses to clear.
+ env: MOCK,
+ }),
+ C('live-commit-mock-applied-page', ['--page-url=/', '--provider=mock'], twoPages, {
+ env: {
+ ...MOCK,
+ IMPECCABLE_LIVE_COPY_AGENT_MOCK_WRITES: JSON.stringify({ 'index.html': '\n\n \n \n Oracle Fixture, renamed
\n A tighter hook.
\n \n \n \n\n' }),
+ IMPECCABLE_LIVE_COPY_AGENT_MOCK_RESULT: JSON.stringify({ status: 'done', appliedEntryIds: ['e1', 'e2'], failed: [], files: ['index.html'], notes: ['oracle mock'] }),
+ },
+ }),
+ C('live-commit-mock-partial', ['--page-url=/', '--provider=mock'], twoPages, {
+ env: {
+ ...MOCK,
+ IMPECCABLE_LIVE_COPY_AGENT_MOCK_WRITES: JSON.stringify({ 'index.html': 'Oracle Fixture, renamed
Minimal static page for oracle live-mode goldens.
\n' }),
+ IMPECCABLE_LIVE_COPY_AGENT_MOCK_RESULT: JSON.stringify({ status: 'partial', appliedEntryIds: ['e1'], failed: [{ entryId: 'e2', reason: 'ambiguous duplicate copy' }], files: ['index.html'] }),
+ },
+ }),
+ C('live-commit-mock-error', ['--provider=mock'], twoPages, {
+ env: { ...MOCK, IMPECCABLE_LIVE_COPY_AGENT_MOCK_RESULT: JSON.stringify({ status: 'error', message: 'agent gave up', appliedEntryIds: [], failed: [], files: [] }) },
+ }),
+ C('live-commit-mock-unreported-file-change', ['--page-url=/', '--provider=mock'], twoPages, {
+ env: {
+ ...MOCK,
+ IMPECCABLE_LIVE_COPY_AGENT_MOCK_WRITES: JSON.stringify({ 'index.html': 'Oracle Fixture, renamed
A tighter hook.
\n' }),
+ IMPECCABLE_LIVE_COPY_AGENT_MOCK_RESULT: JSON.stringify({ status: 'done', appliedEntryIds: ['e1', 'e2'], failed: [], files: [] }),
+ },
+ }),
+ C('live-commit-mock-invalid-result-json', ['--provider=mock'], twoPages, {
+ env: { ...MOCK, IMPECCABLE_LIVE_COPY_AGENT_MOCK_RESULT: '{not json' },
+ }),
+ C('live-commit-mock-leftover-marker-check', ['--page-url=/docs/guide', '--provider=mock'], twoPages, {
+ env: {
+ ...MOCK,
+ IMPECCABLE_LIVE_COPY_AGENT_MOCK_WRITES: JSON.stringify({ 'public/docs/guide.html': '\n\nSecond page lead.
\n\n' }),
+ IMPECCABLE_LIVE_COPY_AGENT_MOCK_RESULT: JSON.stringify({ status: 'done', appliedEntryIds: ['e3'], failed: [], files: ['public/docs/guide.html'] }),
+ },
+ }),
+ C('live-commit-mock-then-discard-remaining', ['--page-url=/docs/guide', '--provider=mock'], twoPages, {
+ env: {
+ ...MOCK,
+ IMPECCABLE_LIVE_COPY_AGENT_MOCK_WRITES: JSON.stringify({ 'public/docs/guide.html': 'Guide
Second page lead.
\n' }),
+ IMPECCABLE_LIVE_COPY_AGENT_MOCK_RESULT: JSON.stringify({ status: 'done', appliedEntryIds: ['e3'], failed: [], files: ['public/docs/guide.html'] }),
+ },
+ steps: [{}, { verb: 'live-discard-manual-edits', args: [] }],
+ }),
+];
diff --git a/tests/oracle/cases/live-roots.mjs b/tests/oracle/cases/live-roots.mjs
new file mode 100644
index 000000000..ac6ceca9e
--- /dev/null
+++ b/tests/oracle/cases/live-roots.mjs
@@ -0,0 +1,124 @@
+/**
+ * `impeccable live` (boot) and `live-inject` corpus: root resolution
+ * (roots.json + repo pointer), config checks, framework injection.
+ * Boot cases stop at config_missing so no helper server is spawned; the one
+ * full boot (`live-boot-full-cycle`) stops its server in a final step.
+ */
+import { LIVE_FILES, fakeGit, rm, write } from '../live-helpers.mjs';
+
+const CFG = '.impeccable/live/config.json';
+const F = LIVE_FILES;
+const PORT_NORM = [
+ ['localhost:\\d{4,5}', 'g', 'localhost:'],
+ ['"(port|serverPort)":(\\s*)\\d{4,5}', 'g', '"$1":$2'],
+ ['Stopped live server on port \\d+\\.', 'g', 'Stopped live server on port .'],
+];
+
+export default [
+ // --- live.mjs (boot) ---
+ { id: 'live-help', verb: 'live', workspace: 'live-html', args: ['--help'] },
+ { id: 'live-target-missing-value', verb: 'live', workspace: 'live-html', args: ['--target'] },
+ { id: 'live-boot-html-config-missing', verb: 'live', workspace: 'live-html', setup: (ws) => rm(ws, CFG), files: F },
+ { id: 'live-boot-vite-config-missing', verb: 'live', workspace: 'live-vite', setup: (ws) => rm(ws, CFG), files: F },
+ { id: 'live-boot-vite-from-subdir', verb: 'live', workspace: 'live-vite', cwd: 'src', setup: (ws) => rm(ws, CFG), files: F },
+ { id: 'live-boot-context-missing-product', verb: 'live', workspace: 'live-vite', setup: (ws) => { rm(ws, 'PRODUCT.md'); }, files: F },
+ { id: 'live-boot-context-missing-design-empty', verb: 'live', workspace: 'live-vite', setup: (ws) => { write(ws, 'DESIGN.md', ''); }, files: F },
+ { id: 'live-boot-context-missing-both', verb: 'live', workspace: 'live-vite', setup: (ws) => { rm(ws, 'PRODUCT.md'); rm(ws, 'DESIGN.md'); }, files: F },
+ { id: 'live-boot-config-invalid-json', verb: 'live', workspace: 'live-html', setup: (ws) => write(ws, CFG, '{ nope'), files: F },
+ { id: 'live-boot-config-invalid-schema', verb: 'live', workspace: 'live-html', setup: (ws) => write(ws, CFG, JSON.stringify({ files: ['index.html'], insertBefore: '
', commentSyntax: 'html' })) },
+ { id: 'live-inject-missing-port', verb: 'live-inject', workspace: 'live-html', args: [], files: F },
+ { id: 'live-inject-port-nan', verb: 'live-inject', workspace: 'live-html', args: ['--port', 'abc'], files: F },
+ { id: 'live-inject-no-config', verb: 'live-inject', workspace: 'live-vite', args: ['--port', '8412'], setup: (ws) => rm(ws, CFG), files: F },
+ { id: 'live-inject-target-missing-value', verb: 'live-inject', workspace: 'live-html', args: ['--check', '--target'] },
+ { id: 'live-inject-html-tag', verb: 'live-inject', workspace: 'live-html', args: ['--port', '8412', '--token', 'tok-oracle'], files: [...F, 'index.html', 'public/**'] },
+ { id: 'live-inject-html-tag-no-token', verb: 'live-inject', workspace: 'live-html', args: ['--port', '8412'], files: [...F, 'index.html'] },
+ { id: 'live-inject-token-from-server-json', verb: 'live-inject', workspace: 'live-html', args: ['--port', '8412'], setup: (ws) => write(ws, '.impeccable/live/server.json', '{"pid":1,"port":8412,"token":"from-server-json"}'), files: [...F, 'index.html'] },
+ { id: 'live-inject-token-server-json-port-mismatch', verb: 'live-inject', workspace: 'live-html', args: ['--port', '8412'], setup: (ws) => write(ws, '.impeccable/live/server.json', '{"pid":1,"port":8499,"token":"other"}'), files: [...F, 'index.html'] },
+ { id: 'live-inject-html-repeat-and-remove', verb: 'live-inject', workspace: 'live-html', files: [...F, 'index.html', 'public/**'], steps: [{ args: ['--port', '8412', '--token', 'a'] }, { args: ['--port', '8413', '--token', 'b'] }, { args: ['--remove'] }, { args: ['--remove'] }] },
+ { id: 'live-inject-remove-clean', verb: 'live-inject', workspace: 'live-html', args: ['--remove'], files: [...F, 'index.html'] },
+ { id: 'live-inject-file-not-found', verb: 'live-inject', workspace: 'live-html', args: ['--port', '8412', '--token', 't'], setup: (ws) => write(ws, CFG, JSON.stringify({ files: ['missing.html', 'index.html'], insertBefore: '', commentSyntax: 'html' })), files: [...F, 'index.html'] },
+ { id: 'live-inject-exclude-glob', verb: 'live-inject', workspace: 'live-html', args: ['--port', '8412', '--token', 't'], setup: (ws) => write(ws, CFG, JSON.stringify({ files: ['**/*.html'], exclude: ['dist/**', 'public/no-body.html'], insertBefore: '\n \n \n ', commentSyntax: 'vue' })), files: F },
+ { id: 'live-boot-sveltekit-config-missing', verb: 'live', workspace: 'live-sveltekit', setup: (ws) => rm(ws, CFG), files: F },
+ { id: 'live-boot-astro-config-missing', verb: 'live', workspace: 'live-astro', setup: (ws) => rm(ws, CFG), files: F },
+ { id: 'live-boot-monorepo-from-root', verb: 'live', workspace: 'live-monorepo', setup: (ws) => { fakeGit(ws); rm(ws, 'website/' + CFG); }, files: [...F, 'website/.impeccable/**'] },
+ { id: 'live-boot-monorepo-from-child', verb: 'live', workspace: 'live-monorepo', cwd: 'website', setup: (ws) => { fakeGit(ws); rm(ws, 'website/' + CFG); }, files: [...F, 'website/.impeccable/**'] },
+ { id: 'live-boot-monorepo-target-file', verb: 'live', workspace: 'live-monorepo', args: ['--target', 'website/src/App.jsx'], setup: (ws) => { fakeGit(ws); rm(ws, 'website/' + CFG); }, files: [...F, 'website/.impeccable/**'] },
+ { id: 'live-boot-monorepo-target-eq', verb: 'live', workspace: 'live-monorepo', args: ['--target=website'], setup: (ws) => { fakeGit(ws); rm(ws, 'website/' + CFG); }, files: [...F, 'website/.impeccable/**'] },
+ { id: 'live-boot-monorepo-no-git-from-root', verb: 'live', workspace: 'live-monorepo', setup: (ws) => rm(ws, 'website/' + CFG), files: [...F, 'website/.impeccable/**'] },
+ {
+ id: 'live-boot-multi-app-selection', verb: 'live', workspace: 'live-monorepo', files: [...F, 'website/.impeccable/**', 'admin/.impeccable/**'],
+ setup: (ws) => { fakeGit(ws); rm(ws, 'website/' + CFG); write(ws, 'admin/astro.config.mjs', 'export default {};\n'); write(ws, 'admin/package.json', '{"name":"admin"}\n'); },
+ },
+ {
+ id: 'live-boot-multi-app-target', verb: 'live', workspace: 'live-monorepo', args: ['--target', 'admin'], files: [...F, 'website/.impeccable/**', 'admin/.impeccable/**'],
+ setup: (ws) => { fakeGit(ws); write(ws, 'admin/astro.config.mjs', 'export default {};\n'); write(ws, 'admin/package.json', '{"name":"admin"}\n'); },
+ },
+ { id: 'live-boot-workspaces-selection', verb: 'live', workspace: 'live-workspaces', setup: (ws) => fakeGit(ws), files: F },
+ { id: 'live-boot-workspaces-target', verb: 'live', workspace: 'live-workspaces', args: ['--target', 'apps/web'], setup: (ws) => fakeGit(ws), files: [...F, 'apps/web/.impeccable/**'] },
+ { id: 'live-boot-workspaces-from-child', verb: 'live', workspace: 'live-workspaces', cwd: 'apps/admin', setup: (ws) => fakeGit(ws), files: [...F, 'apps/admin/.impeccable/**'] },
+ {
+ id: 'live-boot-pointer-accumulates', verb: 'live', workspace: 'live-monorepo', files: [...F, 'website/.impeccable/**', 'admin/.impeccable/**'],
+ setup: (ws) => { fakeGit(ws); rm(ws, 'website/' + CFG); write(ws, 'admin/astro.config.mjs', 'export default {};\n'); },
+ steps: [{ args: ['--target', 'website'] }, { args: ['--target', 'admin'] }, { args: ['--target', 'website'] }],
+ },
+ {
+ // Real boot: background server + inject + drift scan, then stop. Port is
+ // dynamic (first free from 8400), masked per-case.
+ id: 'live-boot-full-cycle', verb: 'live', workspace: 'live-html', files: [...F, 'index.html', 'public/**'], normalize: PORT_NORM,
+ steps: [{ verb: 'live' }, { verb: 'live-inject', args: ['--check'] }, { verb: 'live-server', args: ['stop'] }],
+ },
+
+ // --- live-inject.mjs ---
+ { id: 'live-inject-help', verb: 'live-inject', workspace: 'live-html', args: ['--help'] },
+ { id: 'live-inject-check-ok', verb: 'live-inject', workspace: 'live-html', args: ['--check'] },
+ { id: 'live-inject-check-missing', verb: 'live-inject', workspace: 'live-vite', args: ['--check'], setup: (ws) => rm(ws, CFG) },
+ { id: 'live-inject-check-invalid-json', verb: 'live-inject', workspace: 'live-html', args: ['--check'], setup: (ws) => write(ws, CFG, '[1') },
+ { id: 'live-inject-check-invalid-files', verb: 'live-inject', workspace: 'live-html', args: ['--check'], setup: (ws) => write(ws, CFG, JSON.stringify({ files: [], insertBefore: '', commentSyntax: 'html' })) },
+ { id: 'live-inject-check-invalid-anchor', verb: 'live-inject', workspace: 'live-html', args: ['--check'], setup: (ws) => write(ws, CFG, JSON.stringify({ files: ['index.html'], commentSyntax: 'html' })) },
+ { id: 'live-inject-check-env-config', verb: 'live-inject', workspace: 'live-html', args: ['--check'], env: { IMPECCABLE_LIVE_CONFIG: 'alt.json' }, setup: (ws) => write(ws, 'alt.json', JSON.stringify({ files: ['public/docs/guide.html'], insertAfter: '
', commentSyntax: 'html' })), files: [...F, 'index.html'] },
+ { id: 'live-inject-anchor-missing-all', verb: 'live-inject', workspace: 'live-html', args: ['--port', '8412', '--token', 't'], setup: (ws) => write(ws, CFG, JSON.stringify({ files: ['index.html'], insertBefore: '', commentSyntax: 'html' })), files: [...F, 'index.html'] },
+ { id: 'live-inject-insert-after', verb: 'live-inject', workspace: 'live-html', args: ['--port', '8412', '--token', 't'], setup: (ws) => write(ws, CFG, JSON.stringify({ files: ['index.html'], exclude: ['public/**'], insertAfter: '
', commentSyntax: 'html' })), files: [...F, 'index.html', 'public/**', 'dist/**', 'src/**'] },
+ {
+ id: 'live-inject-vite-csp-meta', verb: 'live-inject', workspace: 'live-vite', files: [...F, 'index.html'],
+ setup: (ws) => write(ws, 'index.html', '\n\n