# Impeccable CLI contract The observable behavior of every `impeccable` verb: arguments, environment, inputs, stdout and stderr byte for byte, exit codes, files written, network. This is the specification an alternate implementation of the scripts has to meet; `tests/oracle/` records goldens against it and replays them. Verb names are the binary's subcommands. Each verb was a script under `skill/scripts/` or a `cli/bin` subcommand when this contract was recorded; the mapping is `JS_VERBS` in `tests/oracle/lib.mjs`, and file references below name those scripts as the source the contract was read from. Since the launcher swap the skill invokes every verb as `{{scripts_path}}/impeccable ` and the scripts are gone from the tree; the contract stands as written. Quoted strings, regexes, and JSON in this document are verbatim from the source, including any em dashes inside user-facing messages; do not "fix" them. Sections: 1. Detect CLI and misc verbs (`detect`, `ignores`, `skills`, `concept-seed`, `serve-question`, `generate-image`) 2. Context and utility verbs (`context`, `doctor`, `pin`, `surface-brief`, `critique-storage`, `palette`, `embed-prompt`, `signals`, `detect-csp`) 3. Design hook (`hook`, `hook-before-edit`, `hooks`) 4. Live mode (`live*`) --- ## 1. Detect CLI and misc verbs Source snapshot: repo `impeccable-second` @ main (f88b2837), 2026-08-17. Node `>=22.18.0`, package `impeccable` v3.6.0, `"type": "module"`, `bin: { impeccable: cli/bin/cli.js }`, `main`/`exports["."]`: `cli/engine/detect-antipatterns.mjs`, `exports["./browser"]`: `cli/engine/detect-antipatterns-browser.js`. Notation: `stdout>` / `stderr>` are literal writes; `exit N`; template strings use `${}` as in the source. All regexes are quoted verbatim. --- ### PART 1 — the `impeccable` binary and the detect engine #### `cli/bin/cli.js` -> `impeccable ` (root dispatcher) - **Invoked from**: README.md "CLI" section (`npx impeccable detect src/`, `npx impeccable ignores ...`), README.npm.md Quick Start, `skill/reference/hooks.md:15` ("Manual `npx impeccable detect` scans use the same project filter config..."), `hooks.md:65` ("Run `npx impeccable detect ` first to see what actually fires there"). - **Args**: `args = process.argv.slice(2)`, `command = args[0]`. - **Dispatch order (exact)**: 1. `!command || command === '--help' || command === '-h'` → print (via `console.log`, stdout) then `exit 0`: ``` Usage: impeccable [options] Commands: detect [file-or-dir-or-url...] Scan for UI anti-patterns and design quality issues ignores Manage detector ignore rules, files, and values help List all available skills and commands install Install impeccable skills into your project or global harness link Symlink skills from a local checkout or submodule update Update skills to the latest version check Check if skill updates are available Options: --help Show this help message --version Show version number Compatibility: impeccable skills Legacy namespace; still supported. ``` 2. `--version` / `-v` → `console.log(pkg.version)` read from `/../../package.json`; `exit 0`. 3. `detect` → `process.argv = [argv0, argv1, ...args.slice(1)]`; dynamic-import `../engine/detect-antipatterns.mjs`, `await detectCli()`. 4. `ignores` or `ignore` → `./commands/ignores.mjs` `run(args.slice(1))`. 5. `skills` → `./commands/skills.mjs` `run(args.slice(1))` (legacy namespace). 6. `command ∈ SKILL_COMMANDS = {'help','install','link','update','check'}` → `skills.mjs run(args)` (note: whole `args`, so `run` sees the verb as `args[0]`). 7. `looksLikeDetectTarget(command)` → detect shorthand: `process.argv = [argv0, argv1, ...args]` then `detectCli()`. Predicate: `arg.startsWith('-') || /^https?:\/\//i.test(arg) || arg.includes('/') || arg.includes('\\') || arg.includes('.') || existsSync(resolve(arg))`. 8. `init` → `console.error('"init" is not a CLI command. Type /impeccable init in your AI coding agent\'s chat (Claude Code, Cursor, Codex, ...), not in this terminal.')`, `exit 1`. (Note: a real path literally named `init` hits rule 7 first — tested: "a real path named init still routes to detect".) 9. otherwise → `console.error(\`Unknown command: "${command}"\n\nTo see a list of supported commands, run:\n impeccable --help\`)`, `exit 1`. - **Top-level catch**: `main().catch(error => { if (error?.code === 'IMPECCABLE_PROMPT_ABORT') { console.log('\nAborted.'); exit 130 } console.error(error?.message || error); exit 1 })`. - **No `live` subcommand exists in the CLI** (README/CLAUDE.md mentions of `npx impeccable live` are aspirational; live mode is driven by `skill/scripts/live*.mjs`, out of scope here). - **Tests**: `tests/skills-cli.test.js` ("root help advertises top-level skills commands", "top-level install aliases the legacy skills install command", "#472" init tests, "a real path named init still routes to detect"). --- #### `cli/engine/detect-antipatterns.mjs` (facade) and `skill/scripts/detect.mjs` (wrapper) -> `impeccable detect` - **Facade**: re-exports registry (`ANTIPATTERNS`, `RULE_ENGINE_SUPPORT`, `getAntipattern`, `getRulesForCategory`, `getRuleEngineSupport`), constants (`SAFE_TAGS`, `BORDER_SAFE_TAGS`, `OVERUSED_FONTS`, `GENERIC_FONTS`, `KNOWN_SERIF_FONTS`), color helpers, `isFullPage`, check fns, `createDetectorProfile`, `summarizeDetectorProfile`, design-system fns (`parseFrontmatter as parseDesignFrontmatter`, `normalizeDesignSystem`, `loadDesignSystemForCwd`, `checkSourceDesignSystem`, `collectStaticDesignSystemFindings`), `detectHtml`, `detectUrl`, `createBrowserDetector`, `detectText`, `extractStyleBlocks`, `extractCSSinJS`, fs helpers (`walkDir`, `hasScannableExtension`, `SCANNABLE_EXTENSIONS`, `SKIP_DIRS`, `buildImportGraph`, `resolveImport`, `detectFrameworkConfig`, `isPortListening`, `FRAMEWORK_CONFIGS`), `formatFindings`, `detectCli`. Main-module guard: `process.argv[1]?.endsWith('detect-antipatterns.mjs') || endsWith('detect-antipatterns.mjs/')` → `detectCli()`. - **`skill/scripts/detect.mjs`** (what the skill invokes as `node {{scripts_path}}/detect.mjs ...`): candidates in order `/detector/detect-antipatterns.mjs` (bundled install layout), then `/../../cli/engine/detect-antipatterns.mjs` (repo layout). If neither exists: `stderr> Error: bundled detector not found.\n`, `exit 1`. Else dynamic-imports via `pathToFileURL` and `await detectCli()`. `detectCli` strips a leading `detect` arg itself, so both `detect.mjs --json x` and `detect.mjs detect --json x` work. - **Skill call sites (quoted)**: - `reference/routing.md:16`: "**If `scan.targets` is non-empty and `setup.platform` is not `ios`/`android`/`adaptive`, run `node {{scripts_path}}/detect.mjs --json ` once** (the bundled detector over local files: no network, no npx; it reads HTML/CSS, so skip it for native projects)." - `reference/critique.md:73`: `node {{scripts_path}}/detect.mjs --json [target]` with "Pass markup files/directories as `[target]`; do not pass CSS-only files. For URLs, skip CLI scan and use browser visualization. ... Exit code 0 = clean; 2 = findings. If the detector entrypoint is missing or fails to load, report deterministic scan unavailable and continue". - `reference/layout.md:28`: `node {{scripts_path}}/detect.mjs --json --scope layout [target files or dirs]`; `reference/typeset.md:27`: `... --scope type ...`. - `reference/audit.native.md:3`: "no browser tooling or `detect.mjs` applies" for native. #### `detectCli()` — `cli/engine/cli/main.mjs` **Arg normalization**: `args = process.argv.slice(2)` with `-json`→`--json`, `-fast`→`--fast`; if `args[0] === 'detect'` drop it. **Flags** (all detected with `args.includes` unless stated): | Flag | Effect | |---|---| | `--json` | JSON output to **stdout** | | `--quiet` | text mode prints only summary line(s) to stderr | | `--help` | print usage (stdout) and `exit 0` — evaluated *after* scope/viewport parsing (so a bad `--scope` still errors first) | | `--no-advisory` | drop findings with `advisory === true` before output/exit-code | | `--fast` | deprecated, ignored; `stderr> Note: --fast is deprecated and ignored. The full scan is fast now and runs every rule.\n` | | `--gpt`, `--gemini` | deprecated, ignored; `stderr> Note: --gpt and --gemini are deprecated and ignored. Generated-UI tells now run by default.\n` | | `--no-config` | `configEnabled=false`: detectionConfig = `{ignoreRules:[],ignoreFiles:[],ignoreValues:[]}`; also disables design system and inline ignores | | `--no-inline-ignores` | `inlineIgnoresEnabled = configEnabled && !flag` | | `--no-design-system` | `designSystemEnabled = configEnabled && !flag && detectionConfig.designSystem?.enabled !== false` | | `--scope ` / `--scope=` | comma-split, trimmed, filtered; repeated occurrences accumulate; the flag+value are spliced out of `args`. Value missing or starting with `--` → `stderr> Error: --scope requires a value. Valid scopes: ${[...RULE_SCOPES].join(', ')}\n`, `exit 1`. Unknown → `stderr> Error: unknown --scope value(s): ${unknown.join(', ')}. Valid scopes: ...\n`, `exit 1`. `RULE_SCOPES` = union of rules' `scopes` = `type`, `layout` (in insertion order: `type` first from `overused-font`, then `layout` from `nested-cards`). | | `--viewport ` / `--viewport=WxH` | regex `/^(\d{2,5})x(\d{2,5})$/i`; failure → `stderr> Error: --viewport requires a WxH value, e.g. --viewport 390x844\n`, `exit 1`. Sets `baseScanOptions.viewport = {width,height}` (browser scans only). Spliced out. | | positional | `targets = args.filter(a => !a.startsWith('--'))` (so a bare `-x` single-dash arg is a *target*, except `-json`/`-fast` which were rewritten) | **Usage text** (`printUsage`, stdout, verbatim): ``` Usage: impeccable detect [options] [file-or-dir-or-url...] Scan files or URLs for UI anti-patterns and design quality issues. Options: --json Output results as JSON --quiet In text mode, only print the final findings count --scope Only report rules in the given design domain (type, layout). Comma-separated. --viewport Browser viewport for URL scans (default 1280x800), e.g. --viewport 390x844 for a mobile-width pass --no-config Do not apply project config, detector ignores, inline ignore comments, or DESIGN.md --no-inline-ignores Do not honor in-file impeccable-disable* ignore comments --no-design-system Do not load local DESIGN.md / .impeccable/design.json context --no-advisory Suppress advisory findings entirely (e.g. em-dash overuse) --help Show this help message Advisory findings: Some rules are advisory: detected and listed in a separate section, but never counted as failures and never changing the exit code. They stay out of the failure count so they never block automation. --no-advisory hides them. Output streams: Human-readable findings go to stderr so stdout stays available for structured output. Use --json for JSON on stdout, or redirect text with 2> findings.txt. Exit status: 0 Scan completed with no primary findings (advisories may still be listed) 1 At least one requested target could not be scanned 2 Scan completed with primary findings Operational failure takes precedence when a multi-target scan is partial. Project config: Respects .impeccable/config.json and .impeccable/config.local.json detector settings: detector.ignoreRules, detector.ignoreFiles, detector.ignoreValues, and detector.designSystem.enabled. Inline ignores: In-file comments waive a finding where it lives and travel with the file: .brand { font-family: Inter } /* impeccable-disable-line overused-font */ // impeccable-disable-next-line bounce-easing: intentional bounce impeccable-disable applies to the whole file; -line / -next-line are scoped. List one or more rule ids (comma-separated), or omit them / use * for all. Detection modes: HTML files Static HTML/CSS analysis (default, catches linked CSS) Non-HTML files Regex pattern matching (CSS, JSX, TSX, etc.) URLs Puppeteer full browser rendering (auto-detected; http(s):// and file:// URLs; accessible linked CSS included) Examples: impeccable detect src/ impeccable detect index.html impeccable detect https://example.com impeccable detect --json . impeccable detect --no-config src/ ``` **Config**: `readDetectionConfig(process.cwd())` (see "Config file" below). Note config is resolved from **cwd**, not from the target. **Design system per target**: `scanOptionsFor(localPath)`: if design system enabled and `localPath` given, `loadDesignSystemForTarget(localPath, {cache})` walks up from the target's own dir (never cwd): a dir with `DESIGN.md`/`Design.md`/`design.md` (directly, or under fallback dirs `.agents/context`, `docs`) is the design root; a dir with a project marker (`.git`, `package.json`, `.impeccable`) but no DESIGN.md is a boundary → no design system; reaching `os.homedir()` or fs root → none. Sidecar candidates: `/.impeccable/design.json`, `/DESIGN.json`, `/DESIGN.json`. Cache key `root:` or `\0none`. Options become `{...baseScanOptions, designSystem}` when found. `design-system.mjs` has **no CLI surface** (no main guard, no argv parsing); it only exports functions. **Input resolution**: 1. If `!process.stdin.isTTY && targets.length === 0` → **stdin mode** (`handleStdin`): read all stdin; try `JSON.parse`; if `parsed.tool_input.file_path` exists on disk → `detectLocalFile(fp, scanOptionsFor(fp))` (hook payload dispatch); else `detectText(input, '', scanOptionsFor(null))`. 2. Else `paths = targets.length ? targets : [process.cwd()]`. `urlRe = /^(?:https?|file):\/\//i`. If more than one URL target, a shared browser is created once via `createBrowserDetector()` (defaults `waitUntil:'load'`, `settleMs:100`), closed in `finally`; a single URL uses `detectUrl` directly (`waitUntil:'networkidle0'`, `settleMs:0`). 3. For each target, in order: - URL: `file:` URLs get `scanOptionsFor(fileURLToPath(url) or null)`; http(s) get `baseScanOptions` (never cwd's design system). Errors: `stderr> Error: ${e.message}\n`, continue. - Else `resolved = path.resolve(target)`; `fs.statSync` failure → `stderr> Warning: cannot access ${target}\n`, continue. - **Directory**: unless `--json`/`--quiet`, `detectFrameworkConfig(resolved)` (see below) and if found probes the port, writing one of three stderr notices: - listening & matched: `\n${name} dev server detected on localhost:${port}.\nFor more accurate results, scan the running site:\n npx impeccable detect http://localhost:${port}\n\n` - listening & !matched: `\n${name} project detected (${basename(configPath)}).\nPort ${port} is in use by another service. Start the ${name} dev server and scan via URL for best results.\n\n` - not listening: `\n${name} project detected (${basename(configPath)}).\nStart the dev server and scan via URL for best results:\n npx impeccable detect http://localhost:${port}\n\n` Then `files = walkDir(resolved).filter(f => !shouldIgnoreDetectionFile(f, cwd, config))`. If `files.length > 50 && stdin.isTTY && !json && !quiet`: `stderr> \nFound ${n} files (${htmlCount} HTML) in ${target}.\nScanning may take a while${htmlCount > 10 ? ' (static HTML/CSS processes each HTML file individually)' : ''}.\nTarget a specific subdirectory to narrow scope.\n` then readline prompt `Continue? [Y/n] ` on stderr; empty or `/^y(es)?$/i` continues; otherwise `stderr> Aborted.\n`, `exit 0`. Then `buildImportGraph(files)` → reverse map; each file scanned with its own options; findings from a file that is imported get `f.importedBy = [basename(importer), ...]` (Set iteration order). - **File**: skipped if `shouldIgnoreDetectionFile`; else `detectLocalFile`. - `detectLocalFile(fp, opts)`: extension (lowercased) in `HTML_EXTENSIONS = {'.html','.htm'}` → `detectHtml(fp, opts)`; else `detectText(readFileSync(fp,'utf-8'), fp, opts)`. 4. Post-filter: `filterDetectionFindings(all, config)` (ignoreRules/ignoreValues), then `filterByScopes(all, scopes)` (keeps findings whose rule declares any requested scope; empty scopes = no filter), then `--no-advisory` drop. 5. Partition `{primary, advisory}` by `f.advisory === true || f.severity === 'advisory'`. Any target that cannot be scanned sets `hadOperationalFailure` (#711): a URL whose browser setup or scan throws, a path `statSync` cannot reach (`stderr> Warning: cannot access `), an unreadable directory or file in a dir walk, and a per-file scan that throws (`stderr> Error: cannot scan : `). A multi-URL scan whose shared browser fails to launch prints its `Error:` once and skips every URL target. **Output and exit codes**: - `allFindings.length > 0`: - json: `stdout> JSON.stringify(allFindings, null, 2) + '\n'` (all findings, advisory ones flagged). - quiet: `stderr> ${primary.length} anti-pattern${n===1?'':'s'} found.\n`; if advisory: `stderr> dim(`${adv} advisory note${adv===1?'':'s'} (not counted).`) + '\n'`. - text: `stderr> formatFindings(all,false) + '\n'`. - `exit(hadOperationalFailure ? 1 : (primary.length > 0 ? 2 : 0))`. - no findings: json → `stdout> []\n`; text/quiet → nothing. `exit(hadOperationalFailure ? 1 : 0)`. - Exit 1 takes precedence over exit 2: findings from the targets that did scan do not turn a partial scan into a complete one (#711). - Any other exit: `1` for arg errors above; uncaught exceptions propagate to `cli.js` catch (`exit 1`). - `dim(text)` = `process.stderr.isTTY ? '\x1b[2m' + text + '\x1b[0m' : text`. This is the **only** ANSI styling in detect output. **Text format** (`formatFindings(findings, false)`): ``` formatFindingsBody(primary): group by f.file preserving first-seen order; for each file: "\n${file}${importNote}" where importNote = items[0].importedBy?.length ? ` (imported by ${items[0].importedBy.join(', ')})` : '' for each item: " ${item.line ? `line ${item.line}: ` : ''}[${item.antipattern}] ${item.snippet}" " → ${item.description}" (U+2192 arrow, 4-space indent) then "\n${primary.length} anti-pattern${1?'':'s'} found." then, if advisory non-empty: "\n" + dim('── Advisory (not counted as failures) ──') (U+2500 box-drawing dashes) each body line individually dim()-wrapped dim(`\n${advisory.length} advisory note${1?'':'s'}. Suppress with --no-advisory.`) all joined with '\n'. ``` Example (non-TTY): ``` /abs/a.css line 3: [side-tab] .card — border-left: 4px solid #6366f1 → Thick colored border on one side of a card — ... 1 anti-pattern found. ── Advisory (not counted as failures) ── /abs/a.html [em-dash-overuse] 9 em-dashes in body text → Em-dash saturation ... 1 advisory note. Suppress with --no-advisory. ``` **Finding object** (`cli/engine/findings.mjs` `finding(id, filePath, snippet, line = 0)`), key order exactly: ```js { antipattern: id, name: ap.name, description: ap.description, severity: ap.severity || 'warning', category: ap.category || null, file: filePath, line, snippet } // plus, only when the effective severity is 'advisory': advisory: true ``` Optional keys added later by engines (appended after the above): `ignoreValue` (design-system rules; browser findings with a value), `importedBy` (dir scans), `severity` may be overwritten by per-finding promotion (browser & html-patterns, e.g. pulsing dot in a header). Design-system findings are `{...finding(...), ...extras}` where extras = `{ ignoreValue }`. Static-HTML and browser findings have `line: 0`; regex findings have 1-based lines. `severity` values in registry: `'warning'` (default), `'advisory'` (many generated-UI tells and design-system-color/radius/font-size, numbered-section-labels, blinking-cursor, shape-assembled-illustration), `'error'` (`script-error`, `content-hidden-at-rest`). `severity` is the canonical advisory field (#709): `deriveAdvisoryFlag` stamps `advisory: true` when and only when the effective severity is `'advisory'`, so a per-finding promotion or demotion carries the flag with it, and every `severity:'advisory'` rule is partitioned out of the failure count and the exit code. `isAdvisory` accepts either `finding.advisory === true` or `finding.severity === 'advisory'`. **Categories**: `category` is `'slop'` (AI tells) or `'quality'`. Category has **no effect on output**, ordering, or exit codes; it is only carried in the finding and used by `getRulesForCategory`. Registry (59 ids, in order): side-tab, border-accent-on-rounded, overused-font, flat-type-hierarchy, gradient-text, ai-color-palette, cream-palette, nested-cards, monotonous-spacing, bounce-easing, pulsing-dot, blinking-cursor, shape-assembled-illustration, dark-glow, radial-halo, radial-spotlight-glow, marquee, icon-tile-stack, italic-serif-display, hero-eyebrow-chip, kicker-above-heading, numbered-section-labels, em-dash-overuse, marketing-buzzword, aphoristic-cadence, oversized-h1, extreme-negative-tracking, broken-image, script-error, content-hidden-at-rest, edge-flush-cards, text-occlusion, first-viewport-column-overflow, gray-on-color, low-contrast, layout-transition, line-length, cramped-padding, body-text-viewport-edge, tight-leading, skipped-heading, heading-rhythm, justified-text, tiny-text, undersized-ui-text, all-caps-body, wide-tracking, text-overflow, repeated-container-text, clipped-overflow-container, design-system-font, design-system-color, design-system-radius, design-system-font-size, gpt-thin-border-wide-shadow, repeating-stripes-gradient, codex-grid-background, theater-slop-phrase, image-hover-transform. Scopes: `type` = overused-font, flat-type-hierarchy, italic-serif-display, hero-eyebrow-chip, kicker-above-heading, numbered-section-labels, oversized-h1, extreme-negative-tracking, line-length, tight-leading, skipped-heading, heading-rhythm, justified-text, tiny-text, undersized-ui-text, all-caps-body, wide-tracking, design-system-font, design-system-font-size; `layout` = nested-cards, monotonous-spacing, icon-tile-stack, content-hidden-at-rest, edge-flush-cards, text-occlusion, first-viewport-column-overflow, line-length, cramped-padding, body-text-viewport-edge, heading-rhythm, text-overflow, clipped-overflow-container. `RULE_ENGINE_SUPPORT = { regex: Set['source','page-analyzer'], 'static-html': Set['element','page'], browser: Set['element','page','layout'], visual: Set['visual-contrast'] }`. **Tests**: `tests/detect-antipatterns-fixtures.test.mjs` (CLI block: `--help exits 0` and contains `Usage:`/`--quiet`, not `--gpt`; `--gpt` prints deprecation; `detect` prefix accepted; should-pass exits 0; should-flag exits 2 with `side-tab` in stderr; `--json` parses; `--quiet` stdout empty and stderr matches `/^[1-9]\d* anti-patterns? found\.$/`; `formatFindings — advisory partitioning`), `tests/detect-cli-stdin-dispatch.test.mjs`, `tests/detect-cli-design-contamination.test.mjs`, `tests/inline-ignores.test.mjs` ("detect CLI end-to-end"), `tests/detect-url-launch.test.mjs`. #### `cli/engine/node/file-system.mjs` - `SKIP_DIRS = {'node_modules','dist','build','__pycache__'}`; any directory whose name starts with `.` is skipped **except** `HIDDEN_SOURCE_DIRS = {'.vitepress','.vuepress','.storybook'}`. The root passed to `walkDir` is never name-checked (an explicit hidden dir scans). - `SCANNABLE_EXTENSIONS = {'.html','.htm','.css','.scss','.sass','.less','.jsx','.tsx','.js','.ts','.vue','.svelte','.astro','.blade.php'}`; `hasScannableExtension` lowercases and also matches multi-dot exts by `endsWith` (`.blade.php`). - `walkDir` returns files in `readdirSync` order, recursive, unreadable dirs → `[]`. - **There is no generated-file detection in the CLI** (`skill/scripts/lib/is-generated.mjs` is hook-side only and not imported by `cli/`). - Import graph: `IMPORT_SPECIFIER_PATTERNS = [/import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g, /@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g, /@(?:use|forward)\s+['"]([^'"]+)['"]/g]`; `resolveImport` only for specifiers matching `/^[./]/`: exact, `base+ext` for each scannable ext, then `base/index+ext`. - `FRAMEWORK_CONFIGS` (first match wins, in this order): Next.js (`next.config.js|mjs|ts`, 3000, `/port\s*[:=]\s*(\d+)/`, header `x-powered-by` ~ `/next/i`); SvelteKit (`svelte.config.js|ts`, 5173, header `x-sveltekit-page` any); Nuxt (`nuxt.config.js|ts`, 3000, `x-powered-by` ~ `/nuxt/i`); Vite (`vite.config.js|ts|mjs`, 5173, body `/@vite\/client/`); Astro (`astro.config.js|ts|mjs`, 4321, body `/astro/i`); Angular (`angular.json`, 4200, `/"port"\s*:\s*(\d+)/`, body `/ng-version/i`); Remix (`remix.config.js|ts`, 3000, `x-powered-by` ~ `/remix/i`). Port overridden by first regex match in the config file. - `isPortListening(port, fingerprint)`: with fingerprint → `fetch('http://localhost:${port}/')` 2s abort, header check then body check → `{listening:true, matched:bool}`; error → `{listening:false}`. Without fingerprint → TCP connect to 127.0.0.1 with 500ms timeout. #### `cli/engine/shared/inline-ignores.mjs` — inline ignore comments - `DIRECTIVE_RE = /impeccable-(disable-next-line|disable-line|disable)\b[ \t]*([^\n\r]*)/gi` (matched anywhere on a line, any comment syntax; case-insensitive). - `TRAILING_CLOSER_RE = /\s*(?:\*\/\}?|--+>|\*\}|#\}|%>|\}\})\s*$/` stripped from the remainder; then reason cut at first `/\s*(?:--+|:)\s*/`; tokens split on `/[\s,]+/`, lowercased; empty or containing `*` → `['*']`. - Lines split on `'\n'` only (CRLF-safe since `\r` excluded from capture). `disable` → file set; `disable-line` on line i (1-based) → `line.get(i)`; `disable-next-line` on line i → `nextLine.get(i+1)`. - `isInlineIgnored(finding)`: rule = lowercased `finding.antipattern`; file set match (`*` or rule) → true; if `line > 0`, `line`/`nextLine` set match → true. Line-less findings (static HTML, browser) match **only** whole-file directives. - Applied inside `detectText` and `detectHtml` at the end unless `options.inlineIgnores === false` (set by `--no-config` or `--no-inline-ignores`). Not applied to URL scans. - Fast path: skip unless `/impeccable-disable/i` occurs. - **DOM-scoped ignore** (`rules/checks.mjs scopedIgnoreActive`): attribute `data-impeccable-ignore="rule-a rule-b"` (split on `/[\s,]+/`, lowercased; empty value or `*` = all) on an element waives matching findings for it and its subtree in browser, extension, and static engines. In `detectHtml`'s html-patterns pass, selector-backed findings are dropped when every element matched by the (pseudo-stripped) selector is under a waiver; unmatched selectors keep the finding. - Tests: `tests/inline-ignores.test.mjs`; fixture `scoped-ignore.html`. #### Config file (`cli/lib/impeccable-config.mjs`) — `.impeccable/config.json` + `.impeccable/config.local.json` - Paths: `/.impeccable/config.json` (shared) and `/.impeccable/config.local.json` (per-dev; `writeDetectionConfig(...,{local:true})` also appends a block to `.git/info/exclude`: `# impeccable-config-ignore-start\n.impeccable/config.local.json\n# impeccable-config-ignore-end`, idempotent by marker regex, following `gitdir:` files for worktrees). - Shape: ```json { "detector": { "ignoreRules": ["side-tab"], "ignoreFiles": ["src/legacy/**"], "ignoreValues": [{ "rule": "overused-font", "value": "inter", "files": ["src/a.css"], "createdAt": "ISO", "reason": "..." }], "designSystem": { "enabled": true }, "advisoryRules": "include"|"exclude" }, "hook": { "consent": "accepted"|"declined", ... }, "updateCheck": true } ``` - `readDetectionConfig(root)`: start `{ignoreRules:[],ignoreFiles:[],ignoreValues:[],designSystem:{enabled:true}}`; for shared then local: apply legacy `raw.hook.*` section then `raw.detector.*`. Arrays are unioned (`uniqueStrings`, String-coerced); ignoreValues merged by key `rule\0value\0sortedFiles.join('\x1f')` (later wins); `designSystem.enabled` false only when literally `false`; `advisoryRules` copied only if `'include'|'exclude'`. Invalid JSON / non-object files are ignored silently. **No validation errors are ever raised by the CLI**; the only validation of ignore lists lives in `skill/scripts/lib/staleness-deep.mjs checkDetectorIgnores` (doctor): unknown `ignoreRules` ids vs live `ANTIPATTERNS` → finding `detector-ignore-rules-unknown` (severity `mention`); non-glob `ignoreFiles` entries that don't exist → `detector-ignore-files-missing`. - `normalizeIgnoreValue(v)`: trim, strip one leading/trailing quote, `+`→space, collapse whitespace, lowercase. Rules lowercased/trimmed. - `normalizeIgnoreValueEntries`: keeps `{rule, value, [files], [createdAt], [reason]}` in **that key order**; `file` (string) and `files` merged, trimmed, deduped. - Glob → regex: `**` → `.*` (swallowing a following `/`), `*` → `[^/]*`, `?` → `[^/]`, `{a,b}` → `(?:a|b)`, regex specials escaped; anchored `^...$`. `matchesAnyGlob` tests the `/`-normalized path and its basename. - `shouldIgnoreDetectionFile(filePath, root, config)`: raw path, absolute path, and root-relative path (if inside root) tested against `ignoreFiles`. - `filterDetectionFindings`: drop when `ignoreRules` has the rule, or an `ignoreValues` entry matches: same rule; entry.value `*` (wildcard) OR extracted value equals (with color-key equality for `design-system-color`: rgb/hex/hsl parsed to `r,g,b,round(a*255)`); if entry has `files`, `finding.file` (or any `/`-suffix of it) must glob-match; a wildcard with no files never matches (unscoped `*` disallowed). - `extractFindingIgnoreValue`: only for `overused-font, bounce-easing, design-system-font, design-system-color, design-system-radius, design-system-font-size`; source `finding.ignoreValue || finding.value`, else parse `detail`/`snippet`: bounce → `animate-bounce`, `cubic-bezier(...)`, or animation token matching `/bounce|elastic|wobble|jiggle|spring/i`; fonts → `Primary font:`, `Google Fonts:`, `font-family:` value, or `family=` URL param (decoded). #### `impeccable ignores` (`cli/bin/commands/ignores.mjs`) - Actions/aliases: `status|ls|list`→list (default when no action), `add-rule|ignore-rule`, `add-file|ignore-file`, `add-value|ignore-value|update-value`, `remove-rule|rm-rule`, `remove-file|rm-file`, `remove-value|rm-value`, `clear`. `--help`/`-h` prints usage (stdout). Unknown → throws `Unknown ignores action: ${a}. Run "impeccable ignores --help".` (exit 1 via cli.js). - Scope flags: `--shared` (default), `--local`, `--all` (remove/clear only); more than one → error `Pass only one scope flag: --shared, --local, or --all` (or `--shared or --local`). - `add-rule [--all-values] [--reason ...]`: `overused-font` without `--all-values` → error "overused-font is value-specific by default. Use add-value overused-font , or add-rule overused-font --all-values for broad suppression." Output: `Added ${rule} to ${local?'local':'shared'} detector ignoreRules (${relpath}).` - `add-file ` → `Added ${glob} to ... detector ignoreFiles (...)`. - `add-value [--file ]... [--reason ]`: value = normalized join of positionals after rule; `--file`/`--files`/`--file=`/`--files=` (empty or flag-like → error); unknown `--x` → `Unknown add-value flag: --x`; `*` value requires `--file`; existing entry (same key) updates reason/files, else pushes `{rule,value,[files],createdAt: ISO now,[reason]}`. Output `Added ${rule}=${value} to ... detector ignoreValues (...)`. - `remove-*` → `Removed ${n} from shared (path), ${n} from local (path).` or `No matching detector ignore found.` `clear` → `Cleared detector ignores in ${'shared and local config'|'local config'|'shared config'}.` - `list` output: ``` Impeccable detector ignores shared file: .impeccable/config.json local file: .impeccable/config.local.json Merged: ignoreRules: (none) ignoreFiles: ... ignoreValues: rule=value [glob1, glob2] - reason, ... designSystem: enabled|disabled Shared: ... Local: ... ``` - Tests: `tests/cli-ignores.test.js`. #### `cli/engine/engines/browser/detect-url.mjs` — URL scans - `detectUrl(url, options)`; options: `profile`, `waitUntil` (default `'networkidle0'`), `settleMs` (default 0), `viewport` (default `{width:1280,height:800}`), `browser` (external), `headless` (default true), `designSystem`, `scriptErrors` (default on), `contentHidden` (default on), `visualContrast`/`visualContrastBrowser`/`visualContrastPixel` (default on), `visualContrastMaxCandidates` (12), `visualContrastScrollOffscreen` (true). - Puppeteer imported dynamically; missing → throw `puppeteer is required for URL scanning. Install: npm install puppeteer`. Browser script read from `/detect-antipatterns-browser.js`; missing → `Browser script not found at ${path}`. - Launch: `launchArgs = process.env.CI ? ['--no-sandbox','--disable-setuid-sandbox'] : []`. On `win32` first `puppeteer.launch({channel:'chrome', headless, args})`, on failure fall back to bundled `puppeteer.launch({headless, args})` with `err.cause = channelError`. Non-Windows: bundled only. - Flow: `newPage` → attach `pageerror` listener (message first line, trimmed, sliced to 160 chars, deduped) → `setViewport(viewport)` → `page.goto(url, {waitUntil, timeout: 30000})` → optional settle → `page.evaluate` sets `window.__IMPECCABLE_CONFIG__ = {...existing, autoScan:false, ...(designSystem ? {designSystem} : {})}` where designSystem serialized as `{present:true, hasFonts, allowedFonts:[...], hasColors, allowedColors:[{r,g,b}], hasRadii, allowedRadii:[px], hasPillRadius}` → `page.evaluate(browserScript)` (injects the bundle; defines `window.impeccableDetect/impeccableDetectAsync/impeccableScan/impeccableScanAsync/impeccableMeasureHiddenText/impeccableCollectVisualContrastCandidates/impeccableAnalyzeVisualContrast/impeccableGetLastVisualContrastAnalyses`) → `window.impeccableDetect({decorate:false, serialize:true})` returns groups `[{selector, tagName, rect, isPageLevel, isHidden, findings:[{type, category, severity, advisory, detail, ignoreValue, name, description}]}]`; flattened to `{id:type, snippet:detail, ignoreValue, severity}` → content-hidden sweep (`measureContentHiddenAfterReveal`: scroll in steps `max(200, floor(innerHeight*0.7))` with `behavior:'instant'`, 40ms rAF pauses, back to top, wait 700ms, then `impeccableMeasureHiddenText()`; `checkContentHiddenAtRest` fires when `totalChars>=200 && hiddenChars>=150 && share>0.3`, snippet `${pct}% of page text (${hidden} of ${total} chars) stays at opacity 0 / visibility hidden after reveal handlers ran (e.g. "sample")`) → up to 3 `script-error` findings `{id:'script-error', snippet:message}` → visual contrast fallback → `finally` close page (and browser if owned). - Visual contrast fallback (`runVisualContrastFallback`): browser-side `impeccableAnalyzeVisualContrast({maxCandidates, scrollOffscreen})` findings not already low-contrast on that selector; then candidates (from analyses or `impeccableCollectVisualContrastCandidates`) not resolved pass/fail → per candidate `captureVisualContrastCandidate(page, candidate, viewport)`. - **Screenshot flow** (`screenshot-contrast.mjs`): clip sanitized (`x,y` floored ≥0; `width` ≤ viewport width (default 1600), `height` ≤ 320, both ≥1); `page.screenshot({encoding:'base64', clip, captureBeyondViewport:true})` before; inject `; Astro:

exactly ONE top-level element, same tag as original
``` Param kinds: `range` `{id,kind:'range',min,max,step,default,label}` → CSS var `--p-`; `steps` `{id,kind:'steps',default,label,options:[{value,label}]}` → attribute `data-p-=""`; `toggle` `{id,kind:'toggle',default:boolean,label}` → `--p-: 1|0` and attribute `data-p-="on"` present only when on. Browser drives range/toggle vars through an injected stylesheet (`#impeccable-live-variant-state`), and hides non-visible variants there; values reset to declared defaults on variant switch. Budget/authoring rules (0-4 per variant, hard cap 4) are prose in live.md section 7. Optional readiness sentinel `--impeccable-variant-ready` (stripped on accept). `cssAuthoring` object returned by wrap/insert: - scoped: `{mode:'scoped', styleTag:' (only when paramValues non-empty)
…variant lines…
``` - JSX: everything above wrapped in `
` … `
` with body indented 2 more, ``, `{/* … */}` comments, `style={{ display: 'contents' }}` on the variant div. Result `{handled:true, file: rel, carbonize:boolean, todo?:'REQUIRED before next poll: carbonize cleanup in . See reference/live.md "Required after accept".'}`. Discard: replace range with deindented original → `{handled:true, file, carbonize:false}`. After accept with `--page-url`, buffered manual-edit ops whose original/new text appears as an exact text segment in the replaced original block are dropped from `pending-manual-edits.json`. Receipt: on any `handled!==false` result write `accept-receipts/.json` = `{id, operation:'accept'|'discard', variantId:'N'|null, result, completedAt}` (tmp+rename). Re-run with same op/variant → prior `result` + `{handled:true, alreadyApplied:true}`; different → `{handled:false, mode:'error', error:'accept_receipt_conflict', priorOperation, priorVariantId}`. Errors: thrown (e.g. `source_locked` from lock contention) → `{handled:false, mode:'error', error:'', file}`; preview-path `{handled:false, error}` without mode gets `mode:'error'` added. CSS helpers (pure, `accept-css.mjs`): `parseStylesheet` (rule/at/comment nodes; `media, supports, layer, container, scope` recurse), `serializeNodes` (`sel {body}` one-line if <60 chars single decl, else multi-line 2-space), `normalizeSelector` (collapse ws, strip ws around `>+~,`), `reconcileCss(existing, incoming)` → replace same-selector bodies (first replaces, later same-selector rules extend), new rules inserted before the first at-block, `{css, replaced, appended}`; `substituteParamVar(css,id,value)` paren-aware `var(--p-id[,fallback])`; `stripParamSelector(sel,id,kind,chosen)`: steps keep only `[data-p-id="chosen"]`/bare form; toggle keeps bare `[data-p-id]` or `="on"` only when on; `bakeParamValues(css, params, values)`: undeclared values bake as `range`; toggle var value `1|0`; drops rules whose every selector died or body empty; strips `--impeccable-variant-ready` declaration; `pruneUnusedSelectors(source, compile, {skipSelectors})`, `collectUnusedSelectors`, `collectAllSelectors`. `verifyAcceptedSource(text)` findings (marker→why): `impeccable-variants-start|end` (`variant wrapper comment left in source`), `impeccable-carbonize-start|end` (`carbonize block not rewritten into permanent form`), `impeccable-param-values` (`param-values comment not baked and removed`), `data-impeccable-` (`live-mode plumbing attribute left on markup`), `/\bdata-p-[A-Za-z0-9_-]+\s*(?:=|\])/` label `data-p-*` (`preview parameter attribute left on markup`), `/var\(\s*--p-[A-Za-z0-9_-]+\s*[,)]/` label `var(--p-*)` (`preview parameter variable not baked to a literal`), `--impeccable-variant-ready` (`preview readiness sentinel left in CSS`). Each finding `{marker, line (1-based), excerpt (≤120), why}`. Source lock: file `/locks/.lock` created `wx` with `{owner, token, pid, at, file}`; stale if unreadable and older than 60 s, or owner pid dead; retry every 5 ms until `waitMs` (accept: 1000) then throw `source_locked` (`code:'SOURCE_LOCKED'`); release only own token. #### 10. Manual edits (staged copy edits) 10.1 Buffer `pending-manual-edits.json`: `{version:1, entries:[{id, pageUrl, element, ops:[op], stagedAt}]}`; op = `{ref, tag, elementId, classes[], originalText, newText, deleted?, leaf?, nearbyEditableTexts?, restore?, sourceHint?:{file,loc,line,column}, contextRef?, container?}`. `stageEntry` merges by (pageUrl, ref): existing op keeps its `originalText`, takes new `newText`/`deleted`, entry.element updated; else appended to entry with same (pageUrl,id) or a new entry. 10.2 Routes: - `POST /manual-edit-stash` body `{token, id, pageUrl, element, ops}` → validate as `manual_edits`; stage; 200 `{ok:true, pendingCount:, totalCount, perPage}`; 500 `{error:'stash_write_failed', message}`; activity `manual_edit_stashed {id,pageUrl,opCount,pendingCount,totalCount,hintedFileCount}`. - `GET /manual-edit-stash?token&pageUrl` → `{count, totalCount, perPage, entries}`. - `POST /manual-edit-commit?token&pageUrl&async=1&repair=1`: `repair=1` without transaction → 409 `{error:'manual_edit_repair_transaction_missing'}`; non-repair first rolls back an abandoned transaction; activity `manual_edit_commit_started`; async → 202 `{status:'started', pendingCount, totalCount, perPage}` immediately, else final result 200 `{…commitResult, totalCount, perPage}` or 500 `{error:'manual_edit_commit_failed', message}`. Provider selection: env `IMPECCABLE_LIVE_COPY_AGENT` (`chat`, `auto` (default), `codex`, `claude`, `mock`, `0|false|off|none`); `chat` when explicit or `auto` with an agent polling within 60 s (`chatAgentLikelyActive`); timeout `IMPECCABLE_LIVE_COPY_AGENT_TIMEOUT_MS` (120000). Chat route pushes `manual_edit_apply` events (10.3). Subprocess route runs `codex exec --cd --dangerously-bypass-approvals-and-sandbox --ephemeral --output-last-message -c model_reasoning_effort="" [--model X] -` or `claude --print --permission-mode bypassPermissions --output-format json [--model X]` with the prompt on stdin **[spawns external CLIs]**. - `POST /manual-edit-repair-decision` `{token?, pageUrl?, action:'rollback'}` → rollback transaction, 200 `{action, pageUrl, rollback, remainingCount, totalCount, perPage}`; other action → 400 `{error:'unsupported_manual_edit_repair_decision', action}`. - `POST /manual-edit-discard?token&pageUrl` → rollback txn, remove entries (page or all), cancel pending apply events (with file rollback), 200 `{discarded:, entries, canceledApplyEvents:[{id,pageUrl,entryCount,rolledBackFiles?,rollbackFailures?}], totalCount, perPage}`. 10.3 `manual_edit_apply` event (server-minted id = 8 hex from uuid): `{type:'manual_edit_apply', id, pageUrl, batch:, evidencePath:'/.impeccable/live/manual-edit-evidence/.json', agentAction:{kind:'manual_edit_apply', required:'apply_source_edits_then_reply', replyCommand:"live-poll.mjs --reply done --data ''", warning:'Polling only leases this work item; it does not commit source edits.'}, schemaVersion:1, deadlineMs:120000 (IMPECCABLE_LIVE_APPLY_EVENT_SOFT_DEADLINE_MS), chunk?:{index,total,opCount,totalOpCount}, repair?:{attempt,maxAttempts,transactionId,reason,failures,files,pageUrl}}`. Batches over `IMPECCABLE_LIVE_MANUAL_EDIT_CHUNK_SIZE` (default 3, 1..20) ops are split into chunks. Hard timeout `IMPECCABLE_LIVE_APPLY_EVENT_HARD_TIMEOUT_MS` (150000): event acked, id tombstoned (late reply → 409 stale + rollback), files snapshot rolled back. Compacted batch: `{version, pageUrl, count, entries:[{id,pageUrl,stagedAt,element:{ref,tagName,id,classes,textContent≤240},ops:[{entryId,ref,contextRef,tag,elementId,classes,originalText,newText,deleted?,sourceHint,leaf,nearbyEditableTexts(≤4),container,contextHints(≤8)}]}], ops:[…flat with entryId], candidates?:[≤24 {entryId,ref,sourceHint,textMatches(≤8),objectKeyMatches(≤8),contextTextMatches(≤8),locatorMatches(≤6)} each match {file,line,column,reason,status}], context?:{bufferPath,totalEntries,totalOps,chunkIndex,chunkTotal,totalApplyOps}}`. 10.4 Apply result (`--data`): `{status:'done'|'partial'|'error', appliedEntryIds:string[], failed:[{entryId, reason, candidates?}], files:string[], notes:string[], message?}`. Rejections (`reason`): `missing_result_data`, `summary_result_not_allowed` (has `entries`/`ops`), `invalid_status`, `_must_be_array`, `appliedEntryIds_must_contain_strings`, `files_must_contain_strings`, `notes_must_contain_strings`, `failed_must_contain_objects`, `failed_entryId_required`, `failed_reason_required`, `applied_entry_id_not_in_event`, `failed_entry_id_not_in_event`, `done_result_has_failed_entries`, `done_result_missing_applied_entry_ids`, `partial_result_has_no_entries`, `error_result_has_applied_entries`. 10.5 Evidence (`live-manual-edit-evidence.mjs` `buildManualEditEvidence({cwd,pageUrl})`): `{version:1, pageUrl, count, entries, ops:[flattened+contextHints], context:{cwd, bufferPath, totalEntries, totalOps}, candidates:[{entryId, ref, originalText, sourceHint:{…,status:'ok'|'text_not_found_near_hint'|'file_missing'|'generated'|'outside_cwd', relativeFile, excerpt:[{line,text}]}|null, textMatches, objectKeyMatches, locatorMatches, contextTextMatches}]}`; empty buffer → `{pageUrl, count:0, entries:[], ops:[], candidates:[]}`. Search dirs `src, app, pages, components, public, views, templates, site, lib, data` + root files, depth ≤7, extensions `.html,.jsx,.tsx,.vue,.svelte,.astro,.js,.mjs,.ts,.ex,.heex,.eex`, skip `node_modules,.git,.impeccable,.astro,.next,.nuxt,.svelte-kit,dist,build,out,coverage` and generated files. Match `{kind:'text'|'object_key'|'id'|'class'|'tag'|'context', file, line, needle, excerpt≤240}`; limits 8 strong/4 weak text, 8 object-key, 4 locator, 8 context (2 per hint). 10.6 Commit (`commitManualEdits`) result: `{applied:[{id,ref,originalText,newText}], failed:[{id, reason, candidates, failures?, checks?, files?}], files, cleared, count, pageUrl, notes?, warnings?, reason?, message?, rolledBackFiles?, rollbackFailures?, unreportedFiles?, repair?:{status:'repaired'|'needs_decision', attempts, maxAttempts, transactionId, failures?, files?}, needsManualDecision?, totalCount, perPage}`. Reasons: `manual_edit_buffer_invalid`, `no_pending_edits`, `conflicting_apply_result`, `unreported_source_changes`, `missing_applied_entry_ids`, `missing_touched_files`, `not_reported_applied`, `source_verification_failed`, `failed_entry_source_changed`, `rolled_back_due_to_failed_entry_source_changed`, `manual_edit_repair_needs_decision`. Post-apply checks: leftover impeccable markers, JSON parse for `.json`, `@babel/parser` syntax for `.jsx/.tsx/.ts` **[NODE-DEP optional; warning `syntax_parser_unavailable` if missing]**, `node --check` for `.js/.mjs/.cjs`, `package.json scripts["impeccable:manual-edit-validate"]` via shell. Repair attempts `IMPECCABLE_LIVE_MANUAL_EDIT_REPAIR_ATTEMPTS` (default 3, 1..10). --- ### Per script Conventions: every script's "run directly" guard is `process.argv[1]` ending with `.mjs` (or `.mjs/`). Unless noted, output is one JSON object on stdout; agent-facing helpers add `_instructions` only in `live-poll.mjs`. Exit code 0 unless stated. #### `live.mjs` -> `impeccable live` (boot) - Invoked from live.md step 1: "`node {{scripts_path}}/live.mjs`" or "`node {{scripts_path}}/live.mjs --target `" (monorepo). - Args: `--target

` / `--target=

` / `-t

` (strict: missing value → stderr `--target requires a path value.` exit 1); `--help|-h` prints usage, exit 0. - Env: none directly (children inherit `IMPECCABLE_LIVE_CONFIG`). - Flow & outputs (all pretty-printed JSON, 2 spaces, exit 0 unless noted): 1. Workspace monorepo selection (`resolveTargetSelection`, only when no target, cwd is a workspace/monorepo root with discoverable children): `{ok:false, error:'target_selection_required', targetPath:null, projectRoot, repoRoot, targetCandidates:[{name, path, targetExample, …context summary}], hint:'Ask the user which app Impeccable should use, then rerun live from that child app cwd. Use --target only as a fallback or explicit path diagnostic.'}`. 2. `resolveRoots` selection → `{ok:false, error:'target_selection_required', targetCandidates:[{name,path}], hint:'Several apps with a dev-server config exist. Ask the user which one to use, then rerun with --target .'}`. 3. Missing/unreadable/empty PRODUCT.md or DESIGN.md → `{ok:false, error:'context_missing', missing:['PRODUCT.md'?,'DESIGN.md'?], nextCommand:'init'|'document', targetPath, projectRoot, repoRoot, productPath:rel|null, designPath:rel|null}`. 4. `writeRootsManifest(roots)`. 5. `node live-inject.mjs --check` (cwd appRoot, 15 s): not ok → print that JSON (`{ok:false,error:'config_missing'|'config_invalid',path,message?}` or `{ok:false,error:'check_failed',raw}`) + `targetPath, projectRoot, repoRoot`, exit 0. 6. Reuse server if `server.json` pid alive, else `node live-server.mjs --background`; failure → `{ok:false,error:'server_start_failed'}` exit 1. 7. `node live-inject.mjs --port P --token T`; not ok → `{ok:false,error:'inject_failed',detail:,serverPort}` exit 1. 8. Drift scan: `.html` files under `public, src, app, pages` (skipping ignored dirs/dot-dirs) not in resolved files and not user-excluded → `configDrift = {orphans:[≤20], orphanCount, hint:'N HTML file(s) exist but aren\'t in config.files. Consider adding them, or use a glob pattern like "public/**/*.html".'}` else `null`. 9. Success: `{ok:true, serverPort, serverToken, pageFiles:[…resolved], liveConfigPath, configDrift, targetPath, projectRoot:appRoot, repoRoot, roots:{manifest}, hasProduct:true, product:, productPath:rel, hasDesign:true, design:, designPath:rel, hasSurfaceBrief, surfaceBrief:, surfaceBriefPath:rel|null, _instructions:'Open the app URL that serves a pageFiles entry (never serverPort; that is the helper). Then start the poll loop per your harness policy in live.md and re-run node /live-poll.mjs immediately after every event or reply. Every event carries _instructions: follow them; they are the authoritative next step with real ids and paths filled in. A poll that is running is a poll you are SERVICING: never announce you are waiting and idle your turn; stay on the exec session until it returns an event, and never end a turn while a poll is outstanding.'}`. Surface brief resolved from `.impeccable/surfaces` under appRoot, contextRoot, repoRoot (first hit). - Tests: `tests/live-target-context.test.mjs`, `tests/live-roots.test.mjs`, `tests/live-e2e.test.mjs` (`session.liveBoot` for `appDir` fixtures), `tests/live-recovery-commands.test.mjs`. #### `live-server.mjs` -> `impeccable live-server` - Invoked from live.md Cleanup (`node {{scripts_path}}/live-server.mjs stop`), by `live.mjs` (`--background`), by tests directly. - Args: (none) foreground; `--background` (spawn detached child with same args minus flag, wait ≤10 s for a `server.json` whose pid ≠ own, print `{"pid","port","token"}` exit 0; else stderr `Timed out waiting for live server to start.` exit 1); `--port=N`; `stop [--keep-inject]` (fetch `/stop?token=` → stdout `Stopped live server on port P.` or `No running live server found.`; then unless keep-inject run `live-inject.mjs --remove`, print `Removed live script tag from .` when a result line has `removed:true`, else `Note: could not remove live script tag ()`; exit 0); `--help`. - Env: `IMPECCABLE_LIVE_DEBUG_EVENTS`, `IMPECCABLE_LIVE_COPY_AGENT*`, `IMPECCABLE_LIVE_APPLY_EVENT_{HARD_TIMEOUT,SOFT_DEADLINE}_MS`, `IMPECCABLE_LIVE_MANUAL_EDIT_CHUNK_SIZE`, `IMPECCABLE_LIVE_MANUAL_EDIT_REPAIR_ATTEMPTS`. - Behavior: shared model sections 3, 5, 6, 8, 10. Console lines: `[live] lease failed for : `, `[impeccable] Svelte component session cleanup failed: …`, `[impeccable] applied legacy deferred Svelte component accepts: {…}`. - Tests: `tests/live-server.test.mjs` (integration: /health, /status, events, poll, gitignore), `tests/live-poll-stream.test.mjs`, `tests/live-e2e.test.mjs`, `tests/live-event-validation.test.mjs`, `tests/live-poll-lanes.test.mjs`, `tests/live-session-store.test.mjs`, `tests/live-generation-preflight.test.mjs`. #### `live-poll.mjs` -> `impeccable poll` - Invoked from live.md poll loop; `--reply` forms quoted in `_instructions` (see instructions.mjs strings in 6.3/below). - Args: `--stream`, `--timeout=MS` (one-shot total, default 600000), `--types=A,B`, `--ack-timeout=MS` (stream, default 600000), `--reply [--file PATH] [--data JSON] [message]`, `--help`. `--reply` errors (stderr, exit 1): `Usage: node "/live-poll.mjs" --reply [--file path] [--data ''] [message]` + `Missing event id after --reply.` / `The value after --reply must be the event id, not the status "done". Use --reply EVENT_ID done.` / `Missing reply status after event id "X".`; `--data must be valid JSON: `. - Needs `server.json`; else stderr `No running live server found. Start one with: node "/live.mjs"` exit 1. - One-shot: loops `GET /poll?token&timeout=&leaseMs=600000[&types]` until an event or total deadline; prints one JSON line (`console.log(JSON.stringify(event))`) with `_instructions` added by `instructionsForEvent` (unless already present). For `accept`/`discard`: spawns `node live-accept.mjs --id ID (--discard | --variant N) [--page-url U] [--param-values JSON]` (30 s), sets `event._acceptResult` (parse failure/throw → `{handled:false, mode:'error', error}`), then POSTs completion `{id, type: completionType, sourceEventType: event.type, message: _acceptResult.error, file: _acceptResult.file, data: {carbonize:true}?}` where completionType = discard: `discarded` if handled else `error`; accept: `agent_done` if handled&carbonize, `complete` if handled, `error` if mode error or (svelte-component unhandled), else `agent_done`; sets `event._completionAck = {ok:true, type}` (+ `final:false, requiresComplete:true, nextCommand:'live-complete.mjs --id ', message:'Carbonize cleanup must be verified, then the session must be completed explicitly before polling again.'` for carbonize) or `{ok:false, error}`. Stderr banners: manual_edit_apply → 4-line banner starting `Manual Apply action required: edit source, then reply with \`live-poll.mjs --reply done --data ''\`.`; carbonize → `⚠ Carbonize cleanup REQUIRED before next poll. After cleanup, run live-complete.mjs --id . See reference/live.md "Required after accept".` - Stream: stderr `[impeccable-poll] stream mode: one JSON object per line on stdout; use --reply while this process stays running`; after each reply-needing event waits (poll `/status` every 400 ms) until the id leaves `pendingEvents` (else `Timed out waiting for --reply on event ` exit 1); returns on `exit`. - Errors: 401 → `Authentication failed. The server token may have changed.` + `Try restarting: node "…/live-server.mjs" stop && node "…/live.mjs"` exit 1; ECONNREFUSED → `Live server not running. Start one with: …` exit 1; reply non-2xx → `Reply failed: \n\n\n\n<_instructions>` exit 1; other → `Poll failed: ` exit 1. - `_instructions` templates (instructions.mjs; `` = abs scripts dir): `steer` → `Do what the message asks (page edits, navigation help, or a short answer). Then reply exactly once: node /live-poll.mjs --reply steer_done ["optional short toast"] (on failure: --reply error "Short reason"). No pickup ack; poll again immediately after.`; `prefetch`, `variant_mount_failed` (`The browser could NOT render variant N (module: URL): ERR… reply node /live-poll.mjs --reply done --file ; the browser retries on its own. Poll again after the reply.`), `discard` (`Original restored and durable completion acknowledged; nothing to do. Poll again.` or `Completion was not acknowledged: run node /live-complete.mjs --id --discarded, then poll again.`), `manual_edit_apply` (delegate to `impeccable_manual_edit_applier`; reply `--reply done --data '{"status":"done","appliedEntryIds":[...],"failed":[],"files":[...],"notes":[]}'`), `timeout` (`No event arrived; poll again immediately.`), `exit` (`Session over: kill any background poll, then node /live-server.mjs stop (removes the injected script tag). Sweep leftover impeccable-variants-start / impeccable-carbonize-start markers from source.`), `generate` (numbered steps: screenshot / scaffold branch (svelte-component: edit stubs `

/v1.svelte…`, params in `params.json`, reply `--file `; deferred wrapper: splice into `scaffold.wrapperBlock` and replace lines `replaceStartLine-replaceEndLine` in ONE edit; written wrapper: splice at `insertLine`; no scaffold: run `live-wrap.mjs --id … --count … --element-id "…" --classes "a,b" --tag "…" --text ""`) / action reference / `When all N variants are delivered: node /live-poll.mjs --reply done --file . Then poll again. If generation fails … reply --reply error "Short reason" …`), `accept` (carbonize 5-step text; `Accept was merged into source mechanically; nothing to clean up. Poll again.`; fallback; `source_locked` retry; `accept_receipt_conflict`; generic error; manual merge). Prefix when ack failed: `Completion was NOT acknowledged: run node /live-status.mjs, finish any cleanup, then node /live-complete.mjs --id . ` - Tests: `tests/live-poll.test.mjs`, `tests/live-poll-stream.test.mjs`, `tests/live-completion.test.mjs`, `tests/live-recovery-commands.test.mjs`. #### `live-status.mjs` -> `impeccable status` - Invoked from live.md Recovery: `node {{scripts_path}}/live-status.mjs`. Args: `--target`. Works with server down. - Output (pretty JSON): `{liveServer:{status,port,connectedClients,agentPolling,pendingEvents}|null, activeSessions:[server list or local store list], render:[{id, renderState, mountedVariants, mountFailures}], recoveryHint}`. Hint: manual apply pending → `Manual Apply pending (page …, chunk i/n, N op(s), N entr(y|ies), likely files: …). If you have not already leased it, run live-poll.mjs. Apply the source edits from the manual_edit_apply batch, then reply with live-poll.mjs --reply done --data ''. Polling only leases this work item; it does not commit source edits. Do not run live-commit-manual-edits.mjs for this leased event. Do not poll again before replying.`; render failed → `The browser failed to mount variant N from URL (ERR); nothing is on screen. Fix the variant files, then reply with live-poll.mjs --reply done --file for the queued variant_mount_failed event (or republish) so the browser retries.`; server up → `Run live-poll.mjs to continue pending work, or live-complete.mjs --id after manual cleanup.`; else `Start live-server.mjs to requeue pending durable events, then run live-poll.mjs.` - Tests: `tests/live-recovery-commands.test.mjs`. #### `live-resume.mjs` -> `impeccable resume` - Args: `--id ID`/`--id=ID`, `--help` (`Usage: node live-resume.mjs [--id SESSION_ID]\n\nPrint the active durable session checkpoint and the next safe agent action.`). - Output: no session → `{active:false, nextAction:'No active durable live session found.'}`; else `{active:true, snapshot, pendingEvent, render:{renderState,mountedVariants,mountFailures}, nextAction}` where nextAction priority: manual apply hint; mount failure text; pending → `Run live-poll.mjs, handle , then acknowledge with live-poll.mjs --reply done.`; phase `carbonize_required` → `Finish carbonize cleanup in , then run live-complete.mjs --id .`; `accept_requested` → `Run live-complete.mjs --id after verifying the accepted variant is written.`; else `Inspect ; no pending agent event is currently queued.` - Read-only (never writes snapshot). Tests: `tests/live-recovery-commands.test.mjs`. #### `live-complete.mjs` -> `impeccable complete` - Invoked from live.md carbonize step and `_completionAck.nextCommand`. Args: `--id ID` (required; missing → usage, exit 1), `--discarded|--discard`, `--error MSG`/`--error=MSG`, `--force`, `--help` (exit 0). - Gate (status complete, no --force): snapshot.sourceFile inside project and not under `node_modules/` → `verifyAcceptedFile`; dirty → `{ok:false, error:'source_dirty', id, file, findings:[…], hint:'The accepted source still carries live-mode leftovers. Finish the carbonize cleanup (bake params, remove markers and data-p-* attributes), then run live-complete again. Use --force only if a finding is a false positive.'}` exit 1. - Then: if server up, `POST /poll {token,id,type:'complete'|'discarded'|'error',message}`; ok → `{ok:true, id, phase, snapshot}`; else append `{type:'complete'|'discarded'|'agent_error', id, message?}` locally → same shape. - Tests: `tests/live-recovery-commands.test.mjs`, e2e `runLiveComplete`. #### `live-accept.mjs` -> `impeccable accept` - Invoked by `live-poll.mjs` automatically; live.md says re-run same command on `source_locked`. - Args: `--id ID` (missing → stderr `Missing --id` exit 1; bad chars → `Invalid --id` exit 1), `--discard` | `--variant N` (`Need --discard or --variant N`; N must be 1-3 digits: `Invalid --variant`), `--param-values ''` (malformed → ignored), `--page-url URL`, `--defer-source-write` (deprecated no-op), `--help`. - Output/side effects: section 9. All results exit 0 (even `handled:false`). - Tests: `tests/live-accept.test.mjs`, `tests/live-accept-scrub.test.mjs`, `tests/live-accept-css.test.mjs`, `tests/live-svelte-component-accept.test.mjs`, `tests/live-source-lock.test.mjs`, e2e `params` scenario. #### `live-wrap.mjs` -> `impeccable wrap` - Invoked from live.md Handle generate step 2 and preflight (`--defer-source-write`). - Args: `--id ID` (req), `--count N` (default 3), `--element-id`, `--classes "a,b"` (comma or space separated), `--tag`, `--query`, `--file PATH`, `--text`, `--page-url URL`, `--defer-source-write`, `--target`, `--help`. Both `--flag value` and `--flag=value` forms accepted. Missing id → `Missing --id` exit 1; none of id/classes/query → `Need at least one of: --element-id, --classes, --query` exit 1. - stderr JSON errors, exit 1: `{error:'element_not_in_source', fallback:'agent-driven', generatedMatch:rel, hint}`, `{error:'element_not_found', fallback:'agent-driven', hint}`, `{error:'file_is_generated', fallback:'agent-driven', file, hint}`, `{error:'Found file but could not locate element in . Searched for: q1, q2'}`, `{error:'element_ambiguous', fallback:'agent-driven', reason?:'rendered_text_not_in_source', file, candidates:[{startLine,endLine}], hint}`, `{error:'missing_page_url_with_pending_edits', pendingEntries:N, hint}`, `{error:'manual_edit_buffer_apply_failed', pendingOps:[{entryId,ref,originalText,reason:'ambiguous_or_unmatched_pending_edit'}], hint}`. - With `--page-url`, buffered manual edits for that page whose text sits in the picked range are applied to the wrapper's "original" copy (source untouched). - stdout success JSON: `{file, sourceFile?, previewMode?:'svelte-component', previewFallback?:{from:'svelte-component', reason}, sourceWritten?:false, wrapperBlock?, replaceStartLine?, replaceEndLine?, componentDir?, propContract?, componentStubMarkup?, sourceStartLine?, sourceEndLine?, startLine, endLine, insertLine, commentSyntax:{open,close}, styleMode:'scoped'|'astro-global-prefixed'|'svelte-component', styleTag:string|null, cssSelectorPrefixExamples:[], cssAuthoring:{…}, originalLineCount}`. Non-deferred, non-svelte: writes the wrapper into the file. Svelte: `file` = manifest path, `startLine=endLine=insertLine=1`. - Tests: `tests/live-wrap.test.mjs`, `tests/live-wrap-buffer-aware.test.mjs`, `tests/live-source-search.test.mjs`, `tests/framework-fixtures.test.mjs` (wrapCases), `tests/live-svelte-ast.test.mjs`. #### `live-insert.mjs` -> `impeccable insert` - Args as wrap plus `--position before|after` (required: `Missing --position (before | after)`, `Invalid --position: X`); no `--page-url`. Errors like wrap (`element_not_in_source`/`element_not_found` with `hint:'See "Handle fallback" in live.md.'`, `file_is_generated`, `element_ambiguous` without hint). - Output: `{mode:'insert', position, file, sourceWritten?:false, wrapperBlock?, replaceStartLine?, replaceEndLine? (= replaceStartLine-1), insertLine, commentSyntax, styleMode, styleTag, cssSelectorPrefixExamples, cssAuthoring}`; svelte: `{mode:'insert', position, file:, sourceFile, previewMode:'svelte-component', componentDir, propContract:[], insertLine:1, sourceInsertLine, anchorStartLine, anchorEndLine, commentSyntax, styleMode:'svelte-component', styleTag:null, cssSelectorPrefixExamples:[], cssAuthoring}`. - Tests: `tests/live-insert.test.mjs`, `tests/live-insert-ui.test.mjs`, `tests/live-e2e/agent-insert.test.mjs`, e2e insert fixtures. #### `live-inject.mjs` -> `impeccable inject` - Invoked by `live.mjs` (`--check`, `--port --token`) and `live-server.mjs stop` (`--remove`); live-setup.md describes config. - Args: `--check` (read-only: `{ok:false,error:'config_missing',path}` exit 0 / `{ok:false,error:'config_invalid',message,path}` / `{ok:true,config,path}`), `--remove`, `--port N` (+ optional `--token T`; without token adopts `server.json` token only when its port matches), `--help`. Missing config in insert/remove → stderr `{ok:false,error:'config_missing',path}` exit 1; `--port` missing/NaN → stderr `{ok:false,error:'missing_port'}` exit 1. - Insert output: tag: `{ok:anyInserted, port, gitIgnore:{file,mode,changed,patterns}, results:[{file, inserted:true, cspPatched}|{file,error:'file_not_found'}|{file,error:'insertion_point_not_found',anchor}], healed?}` (exit 1 if none inserted); adapter: `{ok, port, adapter:'sveltekit'|'nuxt'|'tanstack-start', gitIgnore, results:[adapterResult], healed?}` (exitCode 1 on adapter error). Also writes ignore block at repoRoot when nested. Remove output: `{ok:true, results:[{file, removed, cspReverted}|{file,removed:false,note:'no tag present'}|{file,error:'file_not_found'}], healed?}` or adapter `{ok, adapter, results:[…], healed?}`. - Tests: `tests/live-inject.test.mjs`, `tests/live-frameworks.test.mjs`, `tests/live-tanstack-adapter.test.mjs`, `tests/framework-fixtures.test.mjs`, `tests/live-server.test.mjs` (gitignore). #### `live-target.mjs` - Library only (`resolveLiveTarget(cwd,args)` → `{originalCwd, projectRoot, targetPath, absoluteTargetPath, targetOptions}`); used by `live.mjs`. Tests: `tests/live-target-context.test.mjs`. #### `live-commit-manual-edits.mjs` -> `impeccable commit-manual-edits` - Invoked by `/manual-edit-commit` (server) and manually (`node live-commit-manual-edits.mjs [--page-url=] [--provider=auto|codex|claude|mock]`). live.md/status hint: never run it for a leased chat Apply event. - Env: `IMPECCABLE_LIVE_COPY_AGENT`, `IMPECCABLE_LIVE_COPY_AGENT_TIMEOUT_MS`, `IMPECCABLE_LIVE_COPY_AGENT_MODEL`, `IMPECCABLE_LIVE_COPY_AGENT_EFFORT`, `IMPECCABLE_LIVE_COPY_AGENT_MOCK_RESULT`, `IMPECCABLE_LIVE_COPY_AGENT_MOCK_WRITES`, `IMPECCABLE_LIVE_COPY_AGENT_MOCK_DELAY_MS`, `IMPECCABLE_LIVE_MANUAL_EDIT_REPAIR_ATTEMPTS`. - Output: commit result JSON (10.6); on throw stderr `{error:'commit_failed', message}` exit 1. Does not call enterLiveRoot. - Tests: `tests/live-commit-manual-edits.test.mjs`, `tests/live-copy-edit-agent.test.mjs`. #### `live-discard-manual-edits.mjs` -> `impeccable discard-manual-edits` - Args: `--page-url=` (or `--page-url` bare = true → matches nothing meaningful), `--help` (`Usage: node live-discard-manual-edits.mjs [--page-url=]`). Output `{discarded:, entries:[…removed], totalCount:}`. No enterLiveRoot. Tests: `tests/live-discard-manual-edits.test.mjs`. #### `live-manual-edit-evidence.mjs` - Library (`buildManualEditEvidence`); no CLI main. Tests: via commit tests. #### `live-copy-edit-agent.mjs` - Library: `buildCopyEditBatchPrompt` (long rule list + `Final response contract` JSON shapes), `runCopyEditBatchAgent`, `runCopyEditPostApplyChecks`, `chooseCopyEditAgent`, `parseCopyEditAgentResult` (accepts raw JSON, `{result:""}` wrappers, or first `{…}` in text), `describeNoProviderError`, `extractRunnerErrorMessage`. **[NODE-DEP: spawns codex/claude CLIs, optional @babel/parser]**. Tests: `tests/live-copy-edit-agent.test.mjs`. #### Browser parts (`live-browser-session.js`, `live-browser-dom.js`, `live-browser.js`, `modern-screenshot.umd.js`) - Served concatenated as `/live.js`. localStorage keys: `impeccable-live-session` (`{id, appRoot, state, action, count, expected, arrived, visible, sourceFile, previewFile, previewMode, pageUrl, paramValues, parameterState, insertPlaceholder, pickedAnchor, pickedAnchorViewportTop, pageHash, pageSearch, checkpointRevision}`), `impeccable-live-session-handled` (id), `impeccable-live-session-scroll`, plus prefs keys. Saved sessions with a different `appRoot` are discarded. States: IDLE, PICKING, CONFIGURING, EDITING, GENERATING, CYCLING, SAVING, CONFIRMED. `window.__IMPECCABLE_LIVE_INIT__===true` is the e2e handshake oracle. Polls `/status` periodically for the agent-polling indicator. `modern-screenshot.js` lazy-loaded for annotated captures. - Tests: `tests/live-browser-session.test.mjs`, `tests/live-browser-dom.test.mjs`, `tests/live-browser-regression.test.mjs`, `tests/live-browser-source.test.mjs`, `tests/live-browser-script-parts.test.mjs`, `tests/live-ui-surfaces.test.mjs`, `tests/live-e2e.test.mjs`. #### Preflight (`live/generation-preflight.mjs`, run by the server on lease) - Command: `node live-(wrap|insert).mjs --id --count --defer-source-write [--position P] [--element-id X] [--classes "a b"] [--tag T] [--text <≤80>] [--page-url U (replace only)] [--file ]`; per-target cache keyed on `{mode,position,elementId,classes,tag,pageUrl}` → resolved sourceFile; evicted on failure. Result `{ok:true, mode, durationMs, scaffold:}` or `{ok:false, mode, durationMs, error}` / `{ok:false, skipped:true, reason:'insufficient_locator'}`. Tests: `tests/live-generation-preflight.test.mjs`. #### E2E harness contract (`tests/live-e2e.test.mjs`, `tests/live-e2e/*`) - Fake agent polls `GET /poll?token&timeout=5000` (no lease override → 30 s lease), replies via `POST /poll` with `{token,type:'done',sourceEventType:'generate',id,file}`, `steer_done {message,file}`, `error`, accept/discard completions with `data:{carbonize:true,_acceptResult}`/`{_acceptResult}`, manual apply via `live-poll.mjs --reply done --data `. Variant format: 3 variants (font-weights 300/900/600 for render proof), params `lightness` (range), `face` (steps), `italic` (toggle). Scenarios: core, manual, annotations, exit, missed-done, params, mount-failure, republish, storage-loss (fixtures README). Fixture `runtime` block schema is authoritative for what a reimplementation must satisfy end-to-end.