The Rust engine: one binary replaces every script and the JS detector, fully open (#714)

* Add oracle harness: verb goldens and function-level vectors

Records stdout/stderr/exit/files for every impeccable verb over a fixed
corpus and replays them against an alternate implementation. Adds a loader
hook that captures per-function call vectors from the pure engine modules.

Prepared with AI assistance (Claude Code).

* Oracle: hook, hook-before-edit, hook-admin cases and goldens

Prepared with AI assistance (Claude Code).

* Add docs/CLI-CONTRACT.md: observable behavior of every impeccable verb

Prepared with AI assistance (Claude Code).

* Oracle: context/doctor/pin/surface-brief/critique/palette/embed/signals/csp/seed/genimg/question cases and goldens

Prepared with AI assistance (Claude Code).

* Oracle: live-mode cases and goldens (roots, inject, wrap, insert, accept, session, manual edits, daemon)

Prepared with AI assistance (Claude Code).

* Oracle: mask the binary path before HOME; export launcher env to the binary

Prepared with AI assistance (Claude Code).

* detect: set process.exitCode instead of exiting after the final write

process.exit() right after a large piped stdout write truncated JSON output
at the pipe buffer boundary; found by the oracle harness. Re-record the six
directory-scan goldens that had captured the truncation.

Prepared with AI assistance (Claude Code).

* Oracle: normalize the hook-admin command in both runtimes' forms and audit chars

Prepared with AI assistance (Claude Code).

* Skill text: invoke the impeccable launcher instead of node scripts

Every `node {{scripts_path}}/<name>.mjs` becomes `{{scripts_path}}/impeccable <verb>`
(context-signals -> signals, hook-admin -> hooks). Setup step 1 drops Node, points
Windows shells without sh at impeccable.cmd, and says the launcher runs a
self-contained binary. allowed-tools follows.

Prepared with AI assistance (Claude Code).

* Scripts dir: replace the Node scripts with the impeccable launcher

skill/scripts keeps command-metadata.json and the page JS; every .mjs entry
point, lib/, and live/ are gone (the binary owns those verbs). Adds the POSIX
launcher, impeccable.cmd, VERSION (copied from the new root ENGINE_VERSION),
scripts/fetch-engine.mjs (bun run fetch:engine) to pull the pinned binary
into skill/scripts/bin/<os>-<arch>/, and gitignores that bin dir.

Prepared with AI assistance (Claude Code).

* Build: ship the launcher instead of bundling the JS engine

readSourceFiles no longer copies cli/engine into the skill; the scripts
payload is the launcher (executable bit preserved through dist, plugin/, and
universal.zip), impeccable.cmd, VERSION (synced from ENGINE_VERSION on every
build), the page JS, and command-metadata.json. Hook manifests call
`<scripts>/impeccable hook` behind an existence guard (Codex adds a
commandWindows sibling calling impeccable.cmd; Cursor runs hook-before-edit;
GitHub keeps the git rev-parse form; Grok mirrors Claude); the Node probe and
systemMessage notice are gone. build:release fetches the pinned engine for
every target (lenient) and stages bin/<os-arch>/ into the dist skill copies
after root harness dirs and plugin/ were synced, so git-delivered trees stay
launcher-only. The detection-rule count check reads the vendored
extension/detector/antipatterns.json and is skipped when absent.
build:browser is a stub; the codex prefix rewrite leaves
`{{scripts_path}}/impeccable` alone.

Prepared with AI assistance (Claude Code).

* CLI: turn the impeccable npm package into a platform-binary shim

cli/engine, cli/lib, and cli/bin/commands are gone; their behavior lives in
the engine binary. cli/bin/cli.js now resolves the binary from IMPECCABLE_BIN,
the @impeccable/cli-<os>-<arch> optional dependency (templates under
cli/platform-packages/, published by the engine release), the
~/.impeccable/bin/<version>/ cache, or a checksum-verified download, and
execs it. package.json drops the engine dependencies and the library
exports; puppeteer moves to devDependencies for the icon scripts.
README.npm.md describes the shim.

Prepared with AI assistance (Claude Code).

* Tests: gate behavior on the oracle and the engine binary

Unit tests of the deleted Node scripts and the JS detector are removed;
their behavior is pinned by tests/oracle goldens (frozen JS behavior plus
reviewed deltas) and the engine's own tests. tests/oracle.test.mjs replays
the corpus against the binary (IMPECCABLE_BIN or skill/scripts/bin/<target>/,
via tests/lib/engine-bin.mjs) and skips cleanly without one; the framework
fixture sweep drives live-inject, live-wrap, and detect-csp through the
binary the same way. record.mjs learns --bin. The function-level vectors
under tests/oracle/vectors/calls are committed as the frozen snapshot they
can no longer be regenerated from. Suites: core trimmed to build and
transformer tests, oracle added to the default run, detector/live reduced to
packaging and reference checks, the live-e2e helper tests move to the opt-in
live-e2e lane pending its retarget, cli-remote-e2e is an empty placeholder.

Prepared with AI assistance (Claude Code).

* Docs: describe the launcher, the engine pin, and the oracle gate

CLAUDE.md gains an Engine binary section (launcher lookup order, ENGINE_VERSION,
untracked binaries, how tests get one, the oracle as behavior gate, what stays
JavaScript) and drops the Node-script and JS-detector descriptions; the CLI
and detection-rule sections point at the shim and the engine repo. README.md
states the skill needs no runtime and lists the launcher-based hook commands;
AGENTS.md follows. CLI-CONTRACT.md's intro notes the scripts it quotes are
the recorded source, not the tree.

Prepared with AI assistance (Claude Code).

* Tests: tighten the hook command guard assertion

Prepared with AI assistance (Claude Code).

* Oracle: re-golden 46 cases for the engine's own command names; record them in DELTAS.md

Prepared with AI assistance (Claude Code).

* Build: ship launcher-only release zips by default

IMPECCABLE_BUNDLE_ENGINE=1 opts in to staging the engine binaries into the
dist skill copies. Bundling every target into every provider copy put
dist/universal.zip near 340 MB, past the 25 MB Cloudflare Pages file cap
that impeccable install downloads through.

Prepared with AI assistance (Claude Code).

* Tests: drive the live-e2e orchestrator through the engine binary

The session, fake-agent loop, steer test, and manual-edit probe spawn
<binary> <verb> (live-server, live, live-inject, live-wrap, live-insert,
live-accept, live-poll, live-complete) resolved by tests/lib/engine-bin.mjs
instead of node skill/scripts/live-*.mjs; the completion typing the agent
imported from the deleted live/completion.mjs is a small local helper. The
live-e2e helper unit tests move back into the default live suite (the steer
loop skips without a binary).

Prepared with AI assistance (Claude Code).

* Tests: run new-work-e2e through the engine's serve-question and generate-image verbs

Prepared with AI assistance (Claude Code).

* Tests: point the skill-behavior harness at the launcher and engine binary

The bash tool exports IMPECCABLE_BIN so the staged skill's launcher runs
without a download; scenarios assert on 'impeccable context' instead of
context.mjs and skip without a binary.

Prepared with AI assistance (Claude Code).

* Tests: note what plugin-e2e validates before and after the generated-output sync

Prepared with AI assistance (Claude Code).

* Oracle: record the engine's 'wasm-unsafe-eval' CSP meta patch as a reviewed delta

Prepared with AI assistance (Claude Code).

* Rebase reconciliation: fold main's post-freeze work into the swapped tree

The rebase onto origin/main brought changes whose JS engine halves left the
tree with the swap. This commit reconciles what survives:

- Suite map: register main's comp-fidelity unit tests (build-phase,
  comp-diff, font-match, hero-checks) in the core suite and
  live-browser-ignores in the live suite.
- Payload guard: the skill scripts payload now allowlists the comp-fidelity
  build pipeline (comp-spec/comp-diff/build-phase/font-match and their libs),
  the one Node toolchain that has not moved into the engine.
- Drop skill/scripts/live/project-ignores.mjs, lib/live-path-globs.mjs, and
  their test: they import hook-lib/live-inject/impeccable-paths, which the
  swap deleted, and their consumer (the JS live server) is the engine now.
- skill text: the comp pipeline's calls to engine verbs (generate-image,
  embed-prompt) use the launcher spelling.
- Oracle: re-record 17 detect goldens over the fixture set main changed
  (oklch #592, color-mix #578, 1D grid #615, the two comp-fidelity rules)
  and record the gap in DELTAS.md; those JS rule changes are not yet ported
  to the engine, and the goldens pin its current behavior.

bun run test (oracle included) and bun run build are green on this tree.

AI-assisted change: implemented with Claude Code.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx

* Launcher: engine-probe PATH validation, working .cmd download path; CI: drop stale path, add oracle job

Byte-identical copies of the engine repo's launchers (engine main
af7572c): the retired 3.x npm CLI on PATH or in ~/.impeccable/bin is
rejected by the engine-probe handshake instead of hijacking every verb;
impeccable.cmd's download path is rewritten as straight-line goto flow
(the parenthesized blocks expanded %url%/%cached% at parse time, making
it dead code) with certutil sha256 verification and a windows-arm64 ->
x64 asset fallback; the final error points at the release download
instead of npm i -g (npm still serves the 3.x CLI).

ci.yml: the generated-output check no longer diffs the deleted
cli/engine/detect-antipatterns-browser.js, and a new oracle job fetches
the pinned engine (bun run fetch:engine) and replays tests/oracle/
against it. The job is continue-on-error with a loud warning until the
first engine release exists; flipping it to required is a release-time
toggle, documented in the workflow.

Verified here: sh -n on both launcher copies, bun run build green, full
oracle replay against the rebuilt engine binary green (770 pass, 0
fail), and a launcher behavior test proving a fake 3.x CLI on PATH is
skipped while the download + checksum chain completes against a local
file server.

Prepared with AI assistance (Claude Code).

* Oracle: restore detector goldens to post-fix behavior after the engine ports

The Aug 17-31 detector fixes (oklch parsing, color-mix nested hex, 1D grid
pass, comment stripping, root-relative linked stylesheets, URL userinfo
redaction, inert ignore-value refusal) and the comp-fidelity rules
organic-clip-path / buried-raster are ported to the engine. Re-records the
gap-pinning detect goldens from the fixed binary (glow.html included: its
.photo-opaque-grad column now carries the buried-raster finding it was
written for), replays the frozen checkHtmlPatterns call vectors through the
last JS engine state in history (db1462b9^; args untouched, 14 of 101
results moved), and rewrites the DELTAS gap section into the landed-ports
note. Each re-recorded json fixture golden byte-matches that JS state's
output; oracle: 770 pass, 0 fail.

Prepared with AI assistance (Claude Code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx

* Oracle: pin the Aug 17-31 verb fixes ported to the Rust engine

New cases: hook-session-grok-edit-then-stop (Grok Build camelCase envelope,
end_turn/shutdown/stopHookActive Stop handling, 35ae0733 + bfe634e2 +
3c442af7, #646), hook-session-codex-stop-decision (Codex Stop emits
decision/block, c9e7cd8a, #603), and doctor-order-boot-and-deep (boot and
deep findings keep their established artifact order, 80997663).

Re-recorded goldens whose old bytes froze pre-fix behavior, with a
DELTAS.md entry naming each upstream hash: the Stop finding-cache sync
(3c442af7), the Edit|Write manifests without the retired MultiEdit matcher
(7d5c60d2), and the failWithRollback field order (1f2c3f9d).

Prepared with AI assistance (Claude Code).

* Oracle: drop a duplicated DELTAS section

The verb-fix section landed twice when two porting sessions staged the
same file; keep one copy.

Prepared with AI assistance (Claude Code).

* Oracle: pin the hooks ignore-value inert-entry refusal

Three hadmin-ignore-value-inert-* cases record the engine's port of
be87f5eb (#662) to hooks ignore-value: an exact value for a rule whose
findings can never extract one is refused with the wildcard-plus-file
route (and no config write), while the wildcard scoped form for the same
rule is accepted. Goldens recorded from the engine binary and verified
byte-for-byte against the ea360025 hook-admin.mjs on the same sequences.
No existing golden changes, so no DELTAS entry is owed.

Prepared with AI assistance (Claude Code).

* Launcher: fail closed on a missing download checksum (engine triage C1)

Byte-identical sync of the engine repo's launchers: a freshly downloaded
engine binary now runs only after verifying against its .sha256 sidecar.
A sidecar that cannot be fetched, or a machine with no sha256 tool,
refuses the download instead of exec'ing an unverified binary; the
wget-only path fetches the sidecar too. Binaries already on PATH or in
the cache that pass engine-probe are unaffected.

Prepared with AI assistance (Claude Code).

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx

* Enforce engine-before-skill release order (triage D4)

The launcher, npm shim, and `impeccable install` all resolve the engine
binary for the pinned ENGINE_VERSION, so a skill/CLI release or a rust-swap
merge published ahead of the engine release + platform packages dead-ends
every install path. Add a mechanical guard:

- scripts/check-engine-release.mjs: verifies all five dist binaries +
  .sha256 and the five @impeccable/cli-<os>-<arch> npm platform packages
  exist for the pinned ENGINE_VERSION; names missing assets, exits non-zero.
  Honors IMPECCABLE_DOWNLOAD_BASE.
- release.mjs: hard-fails release:skill and release:cli when assets are
  missing; extension is exempt (vendored WASM detector, no engine exec).
- CI engine-release-ready job: runs the check, continue-on-error with a
  loud ::warning until the first engine release exists (flip to false then).
- CLAUDE.md Releases: documents the enforced ordering.

Prepared with AI assistance (Claude Code).

* Oracle: re-record the Sep-1 verb fixes ported to the Rust engine

Five fixes landed on main in JS between the swap branch and its rebase and were
ported to the engine; the goldens they touch are re-recorded from the fixed
binary, each engine output first diffed byte-for-byte against the upstream JS on
the same inputs. DELTAS.md documents every case with its upstream hash.

- critique-* (usage/unknown/latest-existing/write-then-read/write-monorepo-child):
  the #660 critique close path (identity + fingerprint freshness, ~NNNN
  collision suffix, closed flag, close verb, latest --json). Upstream 5211bdf4.
- detect-* (new overused-font fixture cases, dir/scope/no-advisory sweeps):
  the #678 overused-font primary-face change (a system stack keeps its system
  face, so a Roboto fallback no longer flags). Upstream 2cfd6076.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx

* Tests: fix pre-existing release-guard staging on the swap branch

release.test.mjs was already red on the swap branch: release.mjs imports
check-engine-release.mjs and fetch-engine.mjs (the D4 engine release-order
guard), which the temp work tree never staged, so every dry run failed to
resolve the module instead of exercising the guard. Stage both modules and set
IMPECCABLE_SKIP_ENGINE_CHECK=1 so the guard does not probe the network; this
suite predates the guard and only covers the version/changelog/artifact checks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx

* oracle: pin E8 stale-hook-manifest detector fallback (context)

Cover the v3-to-launcher upgrade fix (triage E8) recorded from the engine
binary and hand-reviewed:

- context-stale-hook-manifest: a .claude/settings.local.json naming the retired
  `node .../hook.mjs` script under the claude-code provider emits
  MANUAL_DETECTOR_REQUIRED, because the stale marker no longer counts as an
  active hook (its script is gone after the update).
- context-launcher-hook-active: the same manifest in the launcher form still
  suppresses MANUAL_DETECTOR_REQUIRED, confirming the launcher marker is
  recognized as active.

The only difference between the two goldens is the MANUAL_DETECTOR_REQUIRED
block. No existing golden moved: every other context case runs under the source
provider, whose hook-manifest list is empty, so none of them scan a manifest.
Also null IMPECCABLE_PROVIDER_ID in the case BASE_ENV so a recording machine's
value cannot leak. DELTAS.md records the intentional divergence from JS parity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx

* Tests: stop two harness hangs from wedging a whole run

Two suites could hang forever and never print a tally, because the one
mechanism that could interrupt the wedged work was missing on both paths.

Hang 1 (bun run test / build-phase.test.mjs): the test's run() helper
spawned every child with spawnSync and no timeout. spawnSync blocks the
test worker's thread, so node's --test-timeout (an event-loop timer)
cannot interrupt a child that wedges (a fork/exec blocked on OS resources
under concurrency, a gate's comp-diff grandchild, or a stray browser
launch). Bound every child with spawnSync timeout + killSignal SIGKILL so
a wedge becomes a fast, named failure the next test survives.

Hang 2 (bun run test:skill-behavior): runTurn called generateText with no
client-side deadline, so a stalled provider stream kept the fetch (and the
whole node process) alive past the per-test timeout, producing no tally.
Attach a real AbortSignal (default 840s, under the 900s per-test cap):
on expiry the fetch aborts, the turn throws, and the scenario
fails-and-continues. The unref'd timer is cleared on completion.

Runner backstops: run-tests.mjs now spawns each command as a detached
process-group leader and enforces a per-suite wall-clock cap that SIGKILLs
the entire group (workers, grandchildren, browsers) on expiry, with
SIGINT/SIGTERM forwarded so Ctrl-C still reaps the tree. The core node
batch gets a finite --test-timeout (180s); skill-behavior gets a 60min
group cap. Env overrides: IMPECCABLE_TEST_WALL_CLOCK_MS,
IMPECCABLE_SKILL_BEHAVIOR_TURN_TIMEOUT_MS,
IMPECCABLE_BUILD_PHASE_RUN_TIMEOUT_MS.

Proof: bun run test green twice (~60s); scoped claude-sonnet-5
skill-behavior sweep terminates with a tally (20 tests, ~32min) where the
840s abort caught a wedged redesign turn and the sweep continued instead
of hanging.

Prepared with AI assistance (Claude Code).

* launcher: export skill-dir env before the IMPECCABLE_BIN exec (sync engine fix)

Prepared with AI assistance (Claude Code).

* Node-free swap: comp-fidelity verbs move to the engine

The four comp-fidelity scripts (comp-spec, comp-diff, font-match, build-phase)
and their six libs are ported into the impeccable-engine binary. This removes
the last Node .mjs from the skill: `git ls-files skill/scripts | grep '\.mjs$'`
now returns nothing.

- reference/new-work.md, reference/visualize.md, and the asset-producer /
  finish-reviewer agents now invoke `{{scripts_path}}/impeccable <verb>` instead
  of `node <script>.mjs`.
- Deleted the ten ported .mjs and the four JS unit tests that imported them
  (their behavior is now covered by the engine's Rust tests and the oracle);
  removed those files from scripts/test-suites.mjs.
- Added oracle cases (comp-*, font-match-*, build-phase-*) over a comp-basic
  workspace, recorded from the engine binary; the deterministic outputs are
  byte-identical to the JS the scripts left behind.
- docs/CLI-CONTRACT.md documents the four verbs, the CDP font rendering, and
  the runtime-resolved (never-committed) font-index catalog.

The font-index catalog JSON stays shipped in the skill (data/font-index.json);
the engine resolves it at run time and never vendors it.

Prepared with AI assistance (Claude Code).

* reorg: public plumbing for the in-repo Rust workspace and the two-release flow

The engine binaries move from the impeccable-dist channel to this repo's own
GitHub Releases (tag engine-v<ENGINE_VERSION>), and the closed detector the
engine links arrives as detector-v<DETECTOR_VERSION> releases on the same
repo. This commit wires the public side for that; the crates themselves land
in the next commit.

- Launcher (sh + cmd), npm shim, fetch-engine and check-engine-release now
  download from github.com/pbakaus/impeccable/releases/download/engine-v<X>/.
- release.mjs gains `engine`: verifies ENGINE_VERSION against the platform
  package pins and the detector release, tags, pushes; release-engine.yml
  builds the five targets and publishes. check-detector-release.mjs is the
  matching release-order guard (with tests).
- Root Cargo.toml (workspace, lto = false with the reason), rust-toolchain.toml
  (exact pin), DETECTOR_VERSION, /target ignored.
- CI: rust + rust-windows jobs and an oracle job that replays the goldens
  against a source build, warn-only until the first detector release exists;
  ci-test-plan exposes a `rust` output.
- docs/ENGINE.md (the crate map and the closed-detector mechanism) and the
  CLAUDE.md engine, release-order and rules sections.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* reorg C: the open Rust runtime joins this repo as one Cargo workspace

The engine no longer lives in a separate repo. `crates/` is a snapshot of the
open crates (foundation, core, common, context, live, hook, skills, comp,
comp-verbs, html, browser, detect, cli) plus `Cargo.lock`, taken as a git
archive of the engine repo at the commit that finished the boundary split.
None of that repo's history comes with it, and none of it should: the closed
half stays private.

The closed half is the rule engine. It ships as a prebuilt native archive per
target, `libimpeccable_detector.a`, published as a `detector-v<X>` GitHub
Release on this repo. `crates/core/build.rs` resolves and links it three ways:
`IMPECCABLE_DETECTOR_LIB=<dir>` for a local detector build, else the
`~/.impeccable/detector/<version>/<target>/` cache, else a download verified
against its `.sha256` sidecar. `crates/core` is a thin shim over a three-symbol
C ABI; nothing above it knows the boundary exists.

What changed versus the engine repo copy:

- Every crate manifest moves from `license-file.workspace` to
  `license.workspace` (this workspace declares Apache-2.0), and the workspace
  gains the `postcard` dependency the boundary encoding needs.
- The launcher contract test reads `skill/scripts/impeccable{,.cmd}` instead of
  a sibling `launcher/` dir, and `engine_binary` downloads from
  `github.com/pbakaus/impeccable/releases/download/engine-v<version>/` instead
  of the retired dist repo. No oracle golden carried the old URL, so no
  re-recording was owed.
- The tests that hunted for a public repo through `IMPECCABLE_PUBLIC_REPO`,
  `../impeccable-second` or a hardcoded home directory now resolve the root as
  `CARGO_MANIFEST_DIR/../..`, because they are in it. The env var stays as an
  override for an out-of-tree checkout.
- The in-page bundle (`detect-antipatterns-browser.js`, 2 MB of generated wasm
  glue) is no longer tracked. `crates/core/build.rs` resolves it beside the
  archive, hands the path to `impeccable_core::browser::IN_PAGE_BUNDLE_JS`, and
  live mode serves that. `scripts/check-detector-release.mjs` now requires it
  and its `.sha256` in a detector release.
- The live crate embeds `skill/scripts/live-browser*.js` and
  `modern-screenshot.umd.js` directly rather than through vendored copies, so
  the binary and the installed skill cannot drift.
- `crates/browser/assets/` (an unused second copy of the bundle) is gone.
- `tests/lib/engine-bin.mjs` also accepts `target/release/impeccable`, so a
  plain `cargo build --release -p impeccable` is enough to run `bun run test`.

Verified with the archive from a local detector build: `cargo test --workspace`
267 pass, oracle 795 pass / 0 fail / 0 missing, `bun run build` clean, the
default suite green, and the launcher's `engine-probe` handshake answering
through `skill/scripts/impeccable`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* docs: bring RUNTIME-ENV and PORTING-GUIDE over with the runtime

They describe the binary's environment contract and the parity method every
crate here was ported with; both belong next to the crates now.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* core/build.rs: refuse a detector archive built by another rustc, in plain words

The archive links only against the exact rustc that built it; a mismatch
used to surface as pages of undefined std symbols from the linker. The
detector repo now writes rustc-version.txt next to the archive (and ships it
with the release); when it is present, build.rs compares it with its own
compiler and names both versions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* build:extension: ship the wasm-core extension shell and vendor its detector from the detector release

`bun run build:extension` was broken on this branch: it still imported the
deleted JS engine (cli/engine/registry/antipatterns.mjs,
scripts/lib/browser-detector-bundle.js).

The shipped shell now matches the new design. The content script only
snapshots the DOM; an extension-owned offscreen document runs the
WebAssembly rule core over that snapshot, so the scanned page's CSP no
longer matters. That replaces the old approach of injecting a JS rules
bundle into the page. New files: extension/offscreen/offscreen.html, plus
the "offscreen" permission and a 'wasm-unsafe-eval' extension_pages CSP in
the manifest.

The manifest version stays at 1.3.3. The shell's own manifest carried
2.0.0; feature branches never bump versions, so the bump is a release step.

The five generated detector pieces (core.js, core_bg.wasm, snapshot.js,
overlay.js, antipatterns.json) are vendored at build time into the
gitignored extension/detector/ by the new scripts/lib/detector-bundle.mjs,
which resolves them the same three ways crates/core/build.rs resolves the
native archive: IMPECCABLE_DETECTOR_LIB/extension-detector/, the
~/.impeccable/detector/<DETECTOR_VERSION>/ cache, then a checksum-verified
download of detector-browser-bundle.zip from the detector release.
antipatterns.json is no longer regenerated here.

The zip packaging is unchanged. The Firefox variant still builds so
`web-ext lint` keeps covering the shared shell, but it cannot scan: Gecko
has no chrome.offscreen API. The build prints a one-line warning saying so.

Also here: a referenced-path check that fails the build when the manifest
or the service worker points at a file that is not in extension/, a
resolver unit test wired into the core suite, and the detector rule count
in the READMEs synced to the 61 the vendored registry carries.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* oracle: replay byte-for-byte on Linux too

The corpus was recorded on macOS and eight cases failed on ubuntu CI for
reasons that were all environment, not behavior:

- stageWorkspace returns the realpath of the staged dir. macOS's tmpdir is a
  symlink and two goldens (context-dir-override, live-accept-source-locked)
  had recorded that artifact; both re-recorded, reviewed in DELTAS.md. The
  source-locked case now actually exercises the lock it is named for.
- context-lowercase-product-name declares platforms: ['darwin', 'win32'];
  run.mjs skips such cases elsewhere and says so in the summary.
- The hook-project workspace's empty provider skill folders (.claude,
  .cursor) are now tracked with .gitkeep; git cannot track empty
  directories, so a fresh checkout had none and hooks on found nothing to
  repair.
- crates/live's read_dir_raw sorts entries by name: the goldens hold the
  order macOS returned, Linux returns hash order, and the source-candidate
  lists in live-commit output depended on it.

macOS: 795 pass, 0 fail. The Svelte accept cases additionally need the
public repo's node_modules on the machine that runs them (CI now installs
them).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* oracle: mask <HOME> only at path boundaries (a short home like /root ate 'roots.json')

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* oracle: track live-html's dist/generated.html (the root dist/ ignore hid it from CI checkouts)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* release-engine: darwin-x64 builds on macos-14 (macos-13 is retired)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Open the detector: the rule crates join the workspace, the C-ABI goes away

The detector is open source. The rules it ships were already public in this
repo's git history and in every npm tarball of the JS engine, so a closed
binary bought nothing it could keep; the moat is the service (the catalog,
the labs, the review pipeline), not the check functions. Keeping them behind
a prebuilt archive cost a C-ABI, an exact toolchain pin, a build-time
download, a second release to order ahead of every engine release, and a
serde layer that had to serve two encodings.

Deleted
- crates/core/src/ffi.rs, crates/core/build.rs, crates/core/tests/boundary.rs
  and the shim modules under src/checks and src/browser.
- crates/foundation/src/boundary.rs and the postcard dependency.
- DETECTOR_VERSION, scripts/check-detector-release.mjs and its test, the
  check:detector-release script, the detector gate and
  IMPECCABLE_SKIP_DETECTOR_CHECK in scripts/release.mjs.
- scripts/lib/detector-bundle.mjs and tests/detector-bundle.test.mjs (the
  vendoring path for the closed browser bundle).
- scripts/build-browser-detector.js and the build:browser script (a stub
  since the JS engine left the tree).
- xtask's detector-archive subcommand and its public-repo lookup.

Came back
- crates/core is now the rule logic itself: every check_* / scan_*, the
  browser adapters, the visual-contrast decisions. It re-exports foundation
  as before, so no consumer changed. Its vectors dispatcher is the union of
  both id tables again, and tests/vectors.rs replays the frozen vectors
  straight through it.
- crates/wasm and crates/xtask join the workspace. cargo xtask bundle builds
  the in-page bundle from browser-bundle/ plus the wasm core, writes
  dist/, refreshes the tracked crates/live/assets/detect-antipatterns-
  browser.js, and writes extension/detector/. bun run build:extension runs
  it instead of downloading.
- crates/live/assets/detect-antipatterns-browser.js is tracked again; live
  mode embeds it and serves it as /detect.js.
- Serde is back to plain derives: no is_human_readable branch in
  js::json_number, derived Serialize for Rgba and BrowserFinding with their
  skip_serializing_if attributes.
- profile.release has lto = "fat" again; rust-toolchain.toml is plain
  stable plus the wasm32 target. The rust, rust-windows and oracle CI jobs
  lose continue-on-error and can be required.

Verified
- cargo build --workspace --all-targets: clean, no warnings.
- cargo test --workspace: 346 pass, 0 fail (the 8 boundary tests are gone
  with the boundary).
- cargo build -p impeccable-wasm --target wasm32-unknown-unknown --release: ok.
- cargo xtask bundle && cargo xtask bundle --check: reproducible; the
  regenerated bundle is committed (it differs from the archived one, which
  was built with a pinned rustc and lto = false).
- cargo build --release -p impeccable: no linker warnings, 12.5 MB (the
  same source at lto = false is 13.1 MB).
- oracle: 795 pass, 0 fail, 0 accepted deltas, 0 missing goldens.
- bun run build, bun run build:extension, web-ext lint (0 errors,
  8 warnings), bun run test: 363 + 80 + 1 + 1 + 133 + 180 + 4 pass, 0 fail.
- impeccable detect --no-config --json tests/fixtures/antipatterns: 128.7 ms
  median of 5.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* core: doc comments drop the open/closed split

The rule crate and the foundation crate are both Apache-2.0 in one
workspace now, so "open", "closed" and "crosses the boundary" no longer
describe anything. Comments only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Rule packs: downstream crates add rules on all three engines; wasm detect surface

A crate that depends on this workspace can now add rules without forking
it. `impeccable_core::rule_pack::RulePack` (object-safe, Send + Sync +
Debug) carries a pack's registry rows plus three hooks that default to
empty: `check_text` for the text engine, `check_element_dom` and
`check_page_dom` for the browser driver. `impeccable_html::StaticRulePack`
adds `check_document` for the static engine, where the document model
belongs to the html crate and detect cannot name it.

The registry keeps ANTIPATTERNS as the built-in list; `registry::extend`
appends a pack's rows and every lookup consults them after the built-ins,
so a pack can never shadow a built-in id (extend panics on a collision and
is idempotent per slice). `all_antipatterns()` is the built-ins followed by
the registered rows.

Hook order, chosen so built-in output cannot move:

- detect_text: after every matcher, analyzer and the dedupe, before inline
  ignores, so `impeccable-disable` waives pack rules like built-in ones.
- detect_html_source: after the element rules, the design-system merge and
  the page passes, again before inline ignores. One pack pass per HTML
  file: the document hook when set, otherwise the text hook over the raw
  source, so a pack implementing both never reports twice.
- collect_browser_findings: the element hook at the end of the per-element
  loop through the same disabled-rules filter and group, the page hook
  after every built-in page pass with the same el-or-body attribution.

A pack travels on TextOptions / ScanOptions, DetectHtmlOptions
(static_rule_pack plus rule_pack), StaticHtmlEngine, and BrowserConfig
(serde-skipped: a pack is a Rust value, not JSON from the page). The
shipped binary installs none.

`crates/wasm --features detect` exposes the two file engines as JSON
exports for hosts that cannot exec the binary: `detect_text_json` and
`detect_html_source_json`, options `{ inlineIgnores?, designSystem? }`,
returning the findings array `detect --json` prints. `antipatterns_json`
now includes a pack's rows. `set_rule_pack` and `set_static_rule_pack` are
Rust-only, for a crate that links this one as an rlib.

Tests: registry extension and collision in foundation, one test pack per
engine (crates/core, crates/detect, crates/html tests) proving each hook
fires, that the built-in findings are unchanged, and that the waivers and
the disabled-rules list cover pack rules, plus the wasm export shapes.
Workspace tests 346 to 361, oracle 795/0 unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* docs: the cutover checklist under the open design

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* bundle: the page JS and the bundler become a library crate downstream packs can reuse

The in-page bundle, the extension pieces, the registry JSON and the
wasm-pack call were reachable only through `cargo xtask bundle`, which read
`browser-bundle/*.js` from the repo root. A downstream crate that links
impeccable-core + impeccable-wasm with its own rule pack had to copy the
page JS to produce a detector bundle for its module.

They move to `impeccable-bundle` (crates/bundle), which embeds every
`browser-bundle/*.js` with `include_str!` and exposes `in_page_bundle`,
`extension_pieces`, `registry_json`, `check_capture_contract` and
`wasm_pack_build`. Nothing writes files or exits the process; the caller
places the bytes. `registry_json` now reads `all_antipatterns()`, so an
installed pack's rows land in `antipatterns.json` too (no built-in change).

xtask becomes the workspace's caller and writes the same files to the same
places; `cargo xtask bundle` is byte-identical, tracked live asset included.
`IMPECCABLE_BUNDLE_SKIP_WASM_PACK` is the skip switch's new name, the old
`IMPECCABLE_XTASK_SKIP_WASM_PACK` still works.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* The immediate tier moves to the registry, and reaches wasm

The design hook's immediate-tier list is the set of rule ids worth fixing
at the edit site, and a downstream reviewer wants the same set to decide
how loudly a finding is reported. `impeccable-hook` is native-only, so the
list moves to `impeccable_core::registry` (the hook re-exports it) and the
`detect` feature gains `immediate_tier_rules_json()`.

The export is behind `detect`, which the in-page bundle does not build, so
the tracked browser asset is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* docs: Pristine tracks the engine by revision pin, not npm

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* docs: the cutover checklist is maintainer-side, not part of the tree

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Port: Fix flat type hierarchy false positives (#702)

Upstream sha 84728e9ce4.

The rule now reads rendered semantic roles and the dominant size per role
instead of the raw set of font sizes on the page, and it fires only when
every adjacent role step is under 1.25x.

- crates/core checks::rules gains TYPE_HIERARCHY_SELECTOR / MIN_ROLES /
  MIN_STEP_RATIO, typeHierarchyRole, dominantTypeRoleSize and
  checkFlatTypeHierarchySamples, the shared half of checks.mjs.
- crates/core browser::page_checks gets checkFlatTypeHierarchyFromDoc over
  the Dom trait, with the overlay skip selector checkTypography passes.
- crates/html page.rs gets the same walk over StaticDocument.
- crates/detect drops the source-only analyzer: flat-type-hierarchy leaves
  REGEX_ANALYZERS, the text-content analyzers shift to index 1, and
  analyzer_rule_id loses its first row.
- crates/html cascade defaults gain contentVisibility, and crates/foundation
  registry carries the reworded description.

Goldens re-recorded (the binary now matches origin/main's JS engine on every
one of these fixtures, verified by scanning the shared corpus with both):
glow, icon-tile-stack, layout, modern-color-borders, motion,
named-color-borders, numbered-section-markers, oklch-neon-text,
typography-should-flag, json and text.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Port: Fix detector URL scans and advisory handling (#709)

Upstream sha fa44839f72.

Advisory handling. `severity` becomes the canonical registry field: the
`advisory` bool leaves `Antipattern`, `advisory_rule_ids` filters on
`severity == "advisory"`, and `derive_advisory_flag` stamps the finding's
`advisory: true` from the effective severity, so a per-finding promotion or
demotion carries the flag. The html and browser engines call it after their
severity override; the detect CLI and the hook accept either spelling; the
driver's serializer and the wasm registry exports derive it the same way.
em-dash-overuse moves from `advisory: true` to `severity: "advisory"`.

URL scans. `expand_joined_url_targets` splits an argv value that is entirely
whitespace-separated URLs and leaves paths with spaces alone. The browser
driver reads the readable linked-stylesheet corpus into the HTML pattern
corpora and resolves a finding's selector with `selector_nodes_for_live_dom`
/ `pseudo_element_host_selector`, so an unresolvable selector drops the
finding instead of keeping it page-level. The CSSOM walk itself is page JS:
`browser-bundle/15-snapshot.js` gains `__snapLinkedStylesheetText` (grouping
rules flattened, container-query probes, effective keyframes) and puts it in
the snapshot as `linkedCss`; `10-probe.js` exposes the same for the in-page
route, and the Dom trait carries `linked_stylesheet_text`.

Also `enclosing_css_selector` blanks comments before hunting the previous
declaration delimiter, and `check_typography` reports the uniquely most-used
family instead of every family over a 15% share.

Verified: `impeccable detect --no-config --json tests/fixtures/antipatterns`
is now byte-identical to `node cli/bin/cli.js` on an origin/main worktree
over the shared corpus (432 findings). The two changed lines in
tests/oracle/vectors/calls/rules.checks/checkHtmlPatterns.jsonl were
re-recorded by running origin/main's `checkHtmlPatterns` over the frozen
args; only the comment-polluted selector changed. Goldens re-recorded for
the advisory partition (config-*, fixture gemini/gpt-tells,
numbered-section-labels, scoped-ignore, shape-assembled-illustration,
color, em-dash-entities) and the help text, each cross-checked against the
JS on origin/main.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Port: stop gray-on-color false positives on Tailwind opacity and JSX (#707)

Upstream sha 32b270f4e8.

`find_solid_chromatic_bg` replaces the bare `bg-<hue>-<n>` match in both
engines: a `bg-blue-500/10` tint is a wash, not a solid fill. The `regex`
crate has no lookahead, so the maximal digit run plus the word boundary is
matched as before and the byte after it is tested for `/`.

The text engine gains the JS-source scanner (`scan_js`) and the scope
helpers on top of it: `containing_markup_tag` keeps a gray text class from
pairing with a background in a sibling tag on the same line, and
`find_ternary_split` / `exclusive_class_scopes` split a `cond ? a : b`
class expression into its arms, recursing into nested ternaries, ignoring
`?.` and `??`, and keeping a common prefix and post-ternary suffix in every
arm. `MatchCtx` now carries the match offset the scope lookup needs.

Verified against origin/main's JS: all eleven cases from the upstream test
file plus a nested / nullish / suffix set produce byte-identical findings on
both engines; they are pinned as Rust unit tests in `regex_matchers` and
`checks::rules`. The shared fixture corpus stays byte-identical.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Port: resolve unique --target names in monorepos (#706)

Upstream sha 8b326fc81e.

`resolve_target_path` / `find_unique_bare_target` in `crates/context`: a
`--target` that does not exist and reduces to a single path segment under
cwd resolves to the one workspace candidate with that name, so `--target a`
selects `apps/a`. A caller that already absolutized the name against cwd
(live and the other helpers do) takes the same route. Ambiguous or unknown
names still report the miss.

The context CLI resolves the target once and hands the resolved path to
`load_context`, replacing `path_exists_for_target`.

Oracle: four new `context-monorepo-target-bare-*` cases (bare name,
absolutized bare name, unknown name, bare name from a child cwd).
`context-monorepo-target-b-inherits` was re-recorded: resolving the target
before `load_context` changes its `surfaceBriefReason` from `not-found` to
`invalid-target`, which is what origin/main's `context.mjs` prints for the
same run.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Port: Fix Next.js 16 CSP and parent hook discovery (#710)

Upstream sha 672ca29642.

CSP. `detect-csp` recognizes Next.js 16's `proxy.{ts,js,mjs}` request hook
beside `middleware.*`, but only where it sits at a project root or its `src/`
directory: the scan root itself, or a nested directory carrying a Next
project marker (a `next.config.*`, an `app` / `pages` dir, or a `next`
dependency). A same-named helper elsewhere in the tree is not the framework
hook.

Context. `find_git_boundary_root` gives `resolve_project` a git-boundary
notion: an explicit target inside its own repository resolves against that
repository, and an external target resolves against its own root, so caller
context never leaks across the boundary. `hook_manifest_search_roots`
replaces the cwd/projectRoot/repoRoot triple with a walk up from the
project root that stops at the first git boundary, and each root's own hook
lifecycle config is honored before its manifest counts as coverage.

Verified against origin/main's JS: nine `detect-csp` placements and five
hook-discovery scenarios (enclosing harness root, that root disabled,
sibling target, nested git target, markerless nested git target) produce
identical output.

Oracle: five `csp-proxy-*` cases and five `context-hook-*` /
`context-markerless-nested-git-target` cases. Four route-target goldens were
re-recorded because #710 resolves a `/`-prefixed target outside the
workspace; each was cross-checked against origin/main, and
`surface-brief-write-route` has a DELTAS entry for the one wording
difference (an unwritable filesystem root).

`tests/framework-fixtures.test.mjs`'s new proxy-placement block came in from
the merge importing the deleted `detectCsp`; it now drives `detect-csp`
through the binary like the rest of that file.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Port: fail URL scans when the browser is unavailable (#711)

Upstream sha f2f9958be1e6a4ecb1fbd5ef1ae1b7d9c53e0d24 (Fix: fail URL scans
when the browser is unavailable).

`detect` gains an operational-failure flag. Exit 1 now means at least one
requested target could not be scanned, and it takes precedence over exit 2,
because findings from the targets that did scan do not turn a partial scan
into a complete one. The flag is set by an unreachable path, an unreadable
directory or file in a dir walk, a per-file scan that throws, a URL scan
that throws, and a shared-browser setup failure.

- `walk_dir_reporting` and `build_import_graph_reporting` take a read-error
  callback; the plain wrappers stay for callers that do not report. A file
  the graph could not read is skipped for the scan too.
- `SharedBrowser::ensure_launched` is the eager half of
  `createBrowserDetector()`: the CLI brings the browser up before the loop so
  a launch failure prints one `Error:` line and every URL target is skipped,
  instead of the lazy launch reporting once per URL.
- The static engine and the text path spell a permission failure the way Node
  does (`EACCES: permission denied, open '<path>'`), which is what
  `Error: cannot scan <target>: <message>` prints.
- Usage text and docs/CLI-CONTRACT.md carry the exit-status block.

Verified against origin/main's JS: missing target, missing target alongside a
flagging file, unreadable file, unreadable file beside a readable sibling,
unreadable directory, unreadable nested directory, a clean scan, and a
browser-unavailable scan of one and of two URLs all agree on exit code,
stdout and stderr (the browser-not-found wording is the pre-existing
puppeteer-vs-discovery difference).

Oracle: `detect-missing-file` and `detect-missing-file-json` re-recorded at
exit 1, plus new `detect-missing-file-with-findings`,
`detect-unreadable-file-json` and `detect-unreadable-file-in-dir`, each
cross-checked against origin/main. `detect-help` carries the new block.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Port: OpenCode slash command bridge (#483)

Upstream sha 9736a9f6e9.

OpenCode does not honor `user-invocable: true` on SKILL.md frontmatter, so a
pinned skill never reaches its slash menu. `pin` now writes
`commands/impeccable-<cmd>.md` on the OpenCode command schema instead, and
skips `.opencode` in the SKILL.md loop so no unreachable
`.opencode/skills/<cmd>` is left behind. `unpin` mirrors it, marker-guarded,
and reaches both scopes even when the skill itself is gone.

`find_opencode_commands_dirs` covers the project-local dir when the project
has the skill and the user config dir when Impeccable is installed globally,
resolving that dir the way the CLI does (`OPENCODE_CONFIG_DIR` ->
`XDG_CONFIG_HOME/opencode` -> `~/.config/opencode`).

The build-tooling half of the upstream change (transformers, the OpenCode
command the build generates, `root-commands-sync`) came in with the merge and
needed no port.

Verified against origin/main's pin.mjs across seven scenarios (no harness,
project scope, user scope, a foreign command file, pin then unpin, unpin over
a foreign file, unpin with nothing pinned): identical stdout, identical file
sets, identical file contents apart from the one deliberate difference.

Oracle: five `pin-opencode-*` cases, with a DELTAS entry for the bridge body
naming the launcher rather than `node .../context.mjs`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Port: Fix Codex skill version metadata (#703)

Upstream sha 482368511a.

Codex's validator rejects unknown top-level keys, so the Codex and `.agents`
skills now carry `version` under the spec-defined `metadata:` map. Both
version readers learn the same parser: `parse_skill_frontmatter_version` in
`crates/context` (the boot update check) and `extract_version` in
`crates/skills` (`getSkillsVersion`). A metadata version wins, a legacy
top-level one still reads, only the map's own indent level counts, tabs count
as two spaces, and a comment line is skipped.

The build-tooling half (`versionInMetadata` on the two providers, the YAML
emitter's nested-object branch) came in with the merge.

Fourteen frontmatter shapes were recorded from origin/main's
`parseSkillFrontmatterVersion` and pinned as unit tests in both crates.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Port: Fix skill subcommand help handling (#708)

Upstream sha a264199177.

`install`, `link`, `update` and `check` render static help before entering
any operational path, through both the top-level verb and the legacy `skills`
namespace, for `--help` and `-h` alike.

Verified against origin/main's `cli/bin/cli.js`: all six spellings produce
identical text and exit codes.

Oracle: a new `tests/oracle/cases/skills.mjs` with seven help cases. Only the
help paths are pinned there; every other installer path writes into harness
directories or reaches the network.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Oracle: goldens for the three fixtures the merge added

`tests/fixtures/antipatterns/` gained `flat-type-hierarchy.html` (#702) and
`linked-url-patterns.{css,html}` (#709) with the merge, so the corpus
generator produced six `detect-fixture-*` cases with no goldens and the
directory-wide cases (`detect-dir-*`, `detect-scope-*`, `detect-no-advisory-*`)
moved.

Every golden here was recorded from the binary and then cross-checked against
`node cli/bin/cli.js` on an origin/main worktree over the same files: the six
per-fixture cases agree byte for byte in JSON and text, and a full scan of
`tests/fixtures/antipatterns` produces 432 findings identical on both engines
after normalizing the repo path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Port: the installer half of the OpenCode command bridge (#483)

Upstream sha 9736a9f6e9, the part of it that
lives in `cli/bin/commands/skills.mjs` rather than `pin.mjs`.

`copy_provider_commands` mirrors `copy_provider_skills` for a provider's
compiled `commands/` dir: project scope writes `<root>/<configDir>/commands`,
user scope writes the config dir OpenCode actually scans
(`OPENCODE_CONFIG_DIR` -> `XDG_CONFIG_HOME/opencode` -> `~/.config/opencode`),
and a pre-#406 global install at `~/.opencode/commands/` loses exactly the
files just written while siblings, symlinked dirs and home-rooted git repos
are left alone. It runs on install, on the reinstall refresh, on update, and
on link, which is the only path that can deliver the bridge to a linked
install.

`is_up_to_date` now compares the bundle's command files too, so an install
whose skills match but whose bridge is missing or drifted refreshes instead of
reporting success while the slash command stays absent. Only bundle-shipped
files are compared, so a pinned shortcut never affects freshness.

`tests/copy-provider-commands.test.js` arrived with the merge importing the
deleted `cli/bin/commands/skills.mjs`; its scenarios are ported to
`crates/skills/tests/provider_commands_tests.rs` (project scope, the three
user-scope dir resolutions, the legacy migration and its two guards, a
provider with no commands dir, and the four `isUpToDate` command-awareness
cases), and the file is removed and deregistered.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* CI: the first full run on the branch, three fixes

- The oracle harness masks the climb to the root a /-prefixed target
  produces (<UP_TO_ROOT>/): the number of `../` is the staged tmpdir's depth
  (7 on macOS, 2 on Linux), not the verb's behavior. surface-brief-path-slash
  re-recorded.
- Two context test helpers canonicalized their temp dir, which on Windows
  yields a \\?\ verbatim path that takes `/` literally; they strip the prefix
  like Node's realpathSync. The critique-storage identity test compares
  against the platform's own resolved path.
- Every job that drives the binary end to end (live-e2e smoke and full,
  accept-cleanup, the DeepSeek sweep, the remote CLI smoke) builds it from
  the checkout first; before, they looked for a release that does not exist.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* context tests: the verbatim-prefix strip spells the prefix once

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* context test: derive the snapshot identity from the verb's own resolver

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* context test: JSON-quote the snapshot identity, as the verb does

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* detect test: import resolution against platform-form paths

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* hook test: the stock cache path in the host's path form; Windows CI runs every crate's tests before failing

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* windows: skills tests pass on Windows

The two test temp roots kept `canonicalize`'s `\\?\` verbatim prefix, and the
kernel takes a verbatim path literally, so every `/`-joined path built under
them was an invalid filename. Strip it the way Node's `realpathSync` does.
The manifest, artifact and sibling-binary expectations hard-coded POSIX
separators for paths the product joins with the host's semantics; derive them
from `jsp::join` instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* windows: hook tests pass on Windows

Same verbatim-prefix strip on the test temp roots, plus expectations derived
from the helpers the product uses: cache keys and scan targets from
`jsp::join`, the config path in an admin message from the same relative form
`path.relative` renders, and the footer hints from `quote_command_arg`, which
deliberately switches to the double-quoted Windows form (#476 / #533). The
env lock no longer poisons the sibling tests when one of them fails.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* windows: html oracle goldens compare on Windows

The goldens pin the `<REPO>`-masked fixture path recorded on POSIX. Mask, then
render the remainder with `/` so a Windows checkout's backslashes are not read
as a finding difference. The goldens are untouched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* windows: widen the live read-deadline test's margin

Timing only. The watchdog polls in 50ms steps against a ~15.6ms Windows system
timer while the crate's tests run in parallel, so the later request takes its
turn later there. The bound stays far under the 60s read timeout a
deadline-less read would hold the ticket for, so the test still distinguishes
the fix from the regression.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* windows: the request read deadline was not enforced on Windows

Windows does not unblock a `recv` already parked in the kernel when another
thread calls `shutdown` on the same socket, so the watchdog could not end a
silent connection's read and it held its turnstile place for the whole 60s
header timeout instead of the 10s deadline. Bound the read at the socket too,
which enforces the same deadline everywhere; the watchdog stays as the backstop
for a connection that trickles bytes without ever completing a request. POSIX
behavior is unchanged: the watchdog already closed the socket at the deadline.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* windows: hook tests derive the rest of the host path forms

The test temp helper's `write` returned a `PathBuf::join` result, which keeps
the `/` inside the relative part and so does not match what the hook resolves a
relative target to on Windows. Three more admin messages and the cache-root slug
pinned the POSIX spelling of paths the product renders with the host's
semantics (`path.resolve` also prefixes the current drive there).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* windows: skills test fixtures name USERPROFILE, and the win32 quoted form

`os.homedir()` reads USERPROFILE on Windows, so a fixture home that named only
HOME sent the global installs into the runner's real profile. The Windows hook
command carries the JSON-quoted path, so a host path's backslashes arrive
escaped; derive the expectation instead of pinning the POSIX spelling.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* windows: check the oracle fixtures out with LF

A finding's snippet carries the scanned file's own bytes, and the goldens were
recorded from a POSIX checkout, so a CRLF checkout of a linked stylesheet reads
as a finding difference. The goldens are untouched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* windows: check the grok global-install manifests as JSON

The Windows hook command carries the JSON-quoted launcher path, so the path's
backslashes are escaped once inside the command and again by the manifest file
itself. Read the manifest as JSON and look for either quoting form instead of
counting escaping layers in a raw substring match.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* npm shim: refuse a download with no verifiable sidecar

The skill launcher and `impeccable install` both fail closed when a
release binary's `.sha256` sidecar cannot be fetched or carries no hash:
they refuse rather than cache an unverified binary. The npm shim did not.
It only compared when a hash was present, so a 404, an empty sidecar, or
a truncated one all wrote the payload straight into
`~/.impeccable/bin/<version>/` and exec'd it.

It now refuses in the same cases, with wording that matches the launcher,
and writes nothing until the hash matches, so a refusal leaves the cache
dir empty. IMPECCABLE_BIN and the optional-dependency lookup are
untouched: neither downloads.

tests/cli-shim.test.mjs runs the real shim against a throwaway HTTP
server and covers missing, empty, and mismatched sidecars, plus the
matching-sidecar and IMPECCABLE_BIN paths. The two refusal cases fail
against the old shim.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Oracle fixture: declare the vite plugin the web workspace imports

`live-workspaces/apps/web/vite.config.js` imports `@vitejs/plugin-react`
but the workspace's package.json listed only `vite`. No oracle case
installs or evaluates that config (the three `live-boot-workspaces-*`
cases stop at root resolution), so the fixture was never wrong at
runtime, only self-contradictory to read. Adding the devDependency keeps
the goldens byte-equal.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Vectors: drop the 12,208 byte-identical repeat lines

The recorder deduplicated by arguments per run, not across runs, so the
frozen call snapshot arrived with 12,208 lines (43% of 28,266) that
repeat an earlier line byte for byte. Every one re-asserts what its first
occurrence already asserts, and `crates/core/tests/vectors.rs` replays
line by line with no count anywhere, so removing them changes nothing it
checks: the replay still reports 8,321 pass, 0 fail.

Duplicates were removed with `awk '!seen[$0]++'`, keeping first
occurrences and file order, and every changed file was checked to equal
that transform of its old contents. No line was added, reordered, or
rewritten, and no vector file gained or lost a distinct call. The tree
drops from 9.2 MB to 5.7 MB.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Fix: restore the live overlay's disabledValues waivers in the engine

The JS engine applied value-level ignore waivers at the tail of
collectBrowserFindings: `_disabledValues` read the entries the live
overlay resolved for the page (skill/scripts/live-browser-ignores.js
sends them as config.disabledValues), and filtered the assembled
findings by the value each one reported, with design-system-color
compared by color value rather than by spelling so a hex waiver
suppressed a finding the browser reported as rgb(...). The Rust port
dropped that stage: `disabledValues` appeared nowhere in the workspace
or in browser-bundle, so a project entry like

    [detector]
    ignoreValues = [{ rule = "overused-font", value = "geist mono" }]

stopped reaching the overlay. The rules the CLI and the edit hook waive
kept drawing markers and counting toward the badge.

Restore it end to end:

* BrowserConfig gains `disabled_values`, parsed leniently so a
  hand-edited __IMPECCABLE_CONFIG__ entry of the wrong shape is dropped
  rather than failing the whole config, the way the JS filter did.
* The driver applies the waivers after every pass, so a rule pack's
  findings are covered the same way the built-in ones are, honoring the
  entries only in extension mode exactly as the JS read them. The
  normalizer, the value extractor (including the rule that bounce-easing
  without a direct ignoreValue offers no value) and the hex/rgb color
  key are ported alongside it.
* collectConfigJson in the in-page bundle and configJson in the
  offscreen bundle forward the field. The extension never sends it, so
  its behavior is unchanged.

Coverage: two driver unit tests (suppression by font value, by hex
waiver across the rgb spelling, and the extension-mode gate; plus the
config parse and the normalizers), a skipScan test that pins the empty
shape for every stage the core produces, and
crates/wasm/tools/disabled-values-check.mjs, a browser-backed check
ported from the retired tests/detect-antipatterns-browser.test.mjs case
that the swap left without a replacement. Against the previous bundle it
fails on exactly the three waiver assertions and passes the skipScan
one, which is the shape of the regression.

Two related review findings were checked and are not defects. skipScan
is gated on extension mode in both the driver and the bundle, which is
what the JS did (index.mjs#skipScanActive), and the live overlay runs in
extension mode: live-browser.js sets `s.dataset.impeccableExtension` on
the injected /detect.js tag, and the overlay's whole detect toggle
travels over the postMessage loop that 50-scan.js installs only under
EXTENSION_MODE. The visual contrast stage is not leaking either:
collectBrowserFindingsAsync and scan() both consult skipScanActive(),
and the offscreen path skips its visual pass on config.skipScan.

The tracked live asset is regenerated (cargo xtask bundle). The oracle
replays with zero unreviewed differences: the new field defaults empty
and the filter is inert without it, and no CLI path sets extension mode.

AI-assisted change: implemented with Claude Code under maintainer
direction.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Shim test: run from a staged copy and prove the download happened

The three fail-closed cases cleared IMPECCABLE_BIN and pointed
IMPECCABLE_HOME at a temp dir, but locate() prefers an installed
@impeccable/cli-<os>-<arch> before the cache or a download. Those
platform packages ship with every engine release and are a merge
prerequisite, so as soon as one is installed under the repo the cases
would resolve it and go green without fetching anything. Confirmed by
hand: with a platform package staged in node_modules, running the shim
against an unreachable download base still exits 0 from the package.

The shim now runs from a throwaway copy at <tmp>/cli/bin/cli.js beside a
copy of the repo's package.json, with no node_modules on the lookup path
above it, so require.resolve of the platform package fails the way it
does on a machine without the optional dependency. Production code is
unchanged; there is no test-only branch in the shim.

The fixture server also records every request now, and each download case
asserts the asset and sidecar URLs were actually requested, so a future
lookup shortcut fails loudly instead of passing on an untested path. A
sixth case installs a fake platform package next to the staged shim and
asserts the shim prefers it with the server untouched, which pins the
precedence the other cases depend on being absent.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Live: the loader now hands off when the resume is the arrival

The overlay could sit in its generating shader over a DOM that already
held all three variants, and only a page refresh cleared it (#719).

The server's generation preflight runs live-wrap with
--defer-source-write, so the wrapper and every variant reach the DOM in a
single HMR batch. The deferred-wrapper scout is constructed at init and
the variant MutationObserver at Go; observer callbacks run in
construction order, so on that batch the scout resumes first and
resumeSession, not the observer, is the transition into CYCLING. It set
the state and the bar but never called hideShaderOverlay(), so the frozen
capture of the original stayed painted over the variants. It also
reported browser_resumed, which does not count as publication progress,
and then disconnected and re-created the observer, dropping the records
that observer had already queued for the same batch, so variants_ready
never fired at all.

resumeSession now finishes the same transition the observer does (shader
down, inline edit off, insert session finalized, params panel rebuilt)
and reports variants_ready when it already holds every variant. The
deferred scout names itself in the journal as
browser_resumed_deferred_wrapper, so the two resume paths are no longer
indistinguishable.

Wrapper resolution goes through findVariantsWrapper, which prefers a
wrapper that actually holds non-original variants. A target inside a
.map() renders one wrapper per item, and an agent that relocates the
wrapper out of the shared primitive live-wrap scaffolded leaves an empty
one behind; first match could pin either and strand the session at 0/N.
With zero or one match this is the querySelector it replaces.

Tests: waitForCycling now asserts the generating shader is gone once the
bar cycles, across every runtime fixture (it failed on vite8-react-plain
before this change and passes after), marked no-retry so the reload
recovery cannot hide it. Source-shape tests pin the transition, the
variants_ready report, and the wrapper preference.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Live server: stop ends the process, SSE skips the mutation lane

Two Rust-only regressions found while investigating #719, both of which
can leave a tab waiting on a broadcast that never comes.

/stop ran shutdown() but never set shutting_down, and the accept loop
only breaks on that flag or a signal, so a stopped server kept its port
and kept answering while its server.json was already deleted. The next
`impeccable live` then booted a second server on another port and a tab
could reattach to the zombie. Node's shutdown() ended in process.exit(0).
The flag is now set after the response is written, so `stop` still reads
"stopping" instead of a reset connection, and the accept loop (already
non-blocking) exits on its next pass.

GET /events took a turnstile ticket and waited its turn before
registering, even though handle_sse releases that ticket two statements
later and needs no arrival ordering. A peer that stalls mid-request holds
the lane for the whole READ_REQUEST_DEADLINE, so a reconnecting stream
could sit unregistered for up to 10 seconds (measured 9.71s against 0.00s
on Node); broadcast is fire-and-forget, so a `done` landing in that
window reaches an empty client set and is gone. Registering early can
only make a stream see more broadcasts. The one cost is that the
connected frame's activeSessions snapshot may miss a mutation still in
flight, and the browser treats that snapshot as a hint. Preflights still
take a turn: answering those out of order reorders the POSTs the browser
issues behind them.

The route classification moved into releases_ticket_up_front so it can be
unit tested. tests/live-server-leak.test.mjs gains a guard that a stopped
server's pid is gone and its port is free.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Live: the shader teardown can no longer race its own construction

The new cycling assertion caught a real defect on CI: vite8-react-insert
reached CYCLING with #impeccable-live-shader still painted over the page.

showShaderOverlay is async. It appends its canvas synchronously, then
awaits createImageBitmap and finishes the GL setup before it publishes
shaderState. hideShaderOverlay returned early on a null shaderState, so a
teardown that landed inside that window did nothing, and the construction
then published itself over a session that had already left GENERATING,
with no teardown left to run. The scroll tick kept repositioning it,
which is why the CI page.html shows the canvas sized from the capture
rect but styled to the cycling anchor.

Every teardown now bumps a shader epoch before it does anything else, and
a construction pins the epoch it owns and abandons its canvas (releasing
the GL context) at every point past an await and before any publish,
including both bitmap-fallback publishes. A teardown also drops a shader
node that no shaderState owns, so an already-orphaned canvas cannot
survive one.

Reproduced by widening the append-to-publish window: with a 400ms delay
after uiAppend, vite8-react-insert failed with the CI error and the probe
showed the teardown arriving at CYCLING with shaderState still null.
The same run passes with this change, as does a 1500ms window on insert
and plain. Locally that window is about 4ms, which is why it only showed
on a slower runner.

The four remaining setLiveState('CYCLING') sites that did not lower the
loader now do: the SSE done handler (the one route that can reach CYCLING
from GENERATING), the Svelte republish remount, and the two accept
failure recoveries.

The e2e assertion already waits up to 5s for the shader to clear, so it
was never racing a legitimate teardown; it is left as it is.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Live: every active-session wrapper lookup goes through the resolver

Cursor Bugbot on #720: findVariantsWrapper alone was not enough.
resolveBarAnchor, the visible-variant element, mountedParameterCount,
readVisibleVariantFromDOM, showVariantInDOM, the source injection, and
the whole accept path still took the first [data-impeccable-variants]
match, so in the relocated-wrapper case Tune never bound and the bar kept
anchoring to the empty scaffold even after the resume reached CYCLING.

Thirteen call sites now resolve through findVariantsWrapper. The resolver
split in two so a missing id cannot silently widen the lookup to any
session: findVariantsWrapper(sessionId) returns null without an id, and
findAnyVariantsWrapper() is the entry point for the two resume paths that
have no id yet. Both share pickPopulatedVariantsWrapper, which is the old
querySelector whenever there are fewer than two matches.

Discard cleanup now hides every duplicate wrapper rather than the first,
since a target inside a `.map()` renders one per item and hiding one left
the rest of the discarded variants on screen.

What still takes a raw first match is deliberate: bare existence checks,
selector strings for stylesheets and observers (which want to cover every
match), querySelectorAll sweeps, the parsed source document, and the
Svelte component wrapper, which holds no variant children at all. The
source-shape test pins that exact set by name, so a new raw lookup fails
until it is either routed through the resolver or justified there.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Live: a discard releases every wrapper it hid

Bugbot on #720: the non-restoreOriginal discard now hides every matching
wrapper, but the delayed fallback still released only the first
querySelector hit. A target inside a `.map()` renders one wrapper per
item, so the rest stayed at display:none and their original content never
came back on the static and missed-HMR flows that fallback exists for.

The hide, the existence checks, and the release now all speak about the
same set. discardedWrappers(sessionId) is the one place that collects it;
releaseDiscardedStaticWrappers takes the stylesheet down once and
releases each wrapper; releaseDiscardedStaticWrapper drops its sessionId
argument and just unwinds the node it is given. The HMR-ownership
decision still reads the first wrapper, which is fair: duplicates all
render from one source element, so ownership is uniform across them. The
reload branch is unchanged because a reload restores every original at
once.

Covered by a source-shape test rather than an e2e scenario:
hasFrameworkHmrOwnership is true for every React, Vue, and Svelte runtime
fixture, so all of them take the watcher path and none can reach the
static release. The existing framework-ownership guards in the same file
move to the new shape and keep their intent, including the one that says
only non-discard cleanup may blank the wrapper while waiting for HMR.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Release: publish the npm platform packages in one command

bun run release:platform-packages downloads each engine-v<ENGINE_VERSION>
binary with its .sha256 sidecar (required; nothing unverified is
published), stages the package from cli/platform-packages/<target> with
the version stamped, the executable at bin/ and the repo LICENSE, and
runs npm publish --access public. Targets already on the registry are
skipped so a re-run resumes after a partial failure. Preconditions:
package.json pins equal ENGINE_VERSION and npm is logged in.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* release-engine: pin checkout, upload-artifact and download-artifact at v7

The v4 pins target Node 20, which the runner now deprecates and forces
onto Node 24 with a warning on every step. The rest of the workflows
already use v7.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Tests: make the temp-dir helpers unique under a coarse clock

Windows' system clock is coarse enough that two parallel tests could get
the same pid-plus-nanoseconds directory name and then remove each
other's files (rust-windows: close_verb_round_trip_and_ownership,
NotFound). A per-process counter is appended to the name.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Tests: declare the temp-dir counter in the hook cache-root tests

The previous commit referenced TMP_SEQ there without defining it.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-09-04 10:42:45 -07:00
committed by GitHub
co-authored by Claude
parent 695df68a58
commit e2ff625b63
1676 changed files with 161105 additions and 110026 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
Run systematic **technical** quality checks on a native app (`ios` / `android` / `adaptive`) and generate a comprehensive report. Don't fix issues; document them for other commands to address.
This is a code-level audit, not a design critique. Audit from source (SwiftUI / UIKit / Compose / React Native / Flutter); no browser tooling or `detect.mjs` applies. Score against the platform reference(s): [ios.md](ios.md) / [android.md](android.md), both for `adaptive`. Read them before scoring if Setup hasn't already. The report skeleton mirrors [audit.md](audit.md); keep the two in sync when changing it.
This is a code-level audit, not a design critique. Audit from source (SwiftUI / UIKit / Compose / React Native / Flutter); no browser tooling or `impeccable detect` applies. Score against the platform reference(s): [ios.md](ios.md) / [android.md](android.md), both for `adaptive`. Read them before scoring if Setup hasn't already. The report skeleton mirrors [audit.md](audit.md); keep the two in sync when changing it.
## Diagnostic Scan
+7 -7
View File
@@ -8,7 +8,7 @@ Resolve one stable target, run two independent assessments, synthesize a design
- Assessment A and B MUST run as two isolated sub-agents whenever a sub-agent/Task tool is exposed. Running them inline in this context is "possible" but is NOT permitted; it is a degraded run. Inline is allowed ONLY when no sub-agent tool exists (or the user declined, on harnesses that ask).
- If you degrade for any reason, the report's first line MUST be a banner: `⚠️ DEGRADED: single-context (<reason>)`. A silent degraded critique is a failed critique.
- Assessment A must finish before detector findings enter the parent synthesis context. Detector output is deterministic, but it still anchors judgment.
- A skipped detector is a failed critique run unless `detect.mjs` is missing or crashes after a real attempt.
- A skipped detector is a failed critique run unless `impeccable detect` is missing or crashes after a real attempt.
- Viewable targets require browser inspection when available.
- Any local server started only for critique visualization must run in the background, have a recorded stop method, and be stopped before final reporting unless the user asks to keep it.
- Do not claim a user-visible overlay exists unless script injection succeeded and the detector ran in the page.
@@ -23,7 +23,7 @@ Resolve one stable target, run two independent assessments, synthesize a design
- "this page" -> the current URL or source file
2. **Confirm the target slugs cleanly**:
```bash
node {{scripts_path}}/critique-storage.mjs slug "<resolved-path-or-url>"
{{scripts_path}}/impeccable critique-storage slug "<resolved-path-or-url>"
```
Every later command also accepts the resolved target directly and derives the same slug internally; never hand-write a slug. If this exits non-zero, skip persistence and trend for this run, but continue the critique.
3. **Read `.impeccable/critique/ignore.md`** if it exists. Drop matching findings silently; it is the only prior-run input critique consumes.
@@ -70,7 +70,7 @@ Run the bundled detector and browser visualization evidence. Assessment B is man
CLI scan:
```bash
node {{scripts_path}}/detect.mjs --json [target]
{{scripts_path}}/impeccable detect --json [target]
```
- Pass markup files/directories as `[target]`; do not pass CSS-only files.
@@ -84,7 +84,7 @@ Browser visualization is required for a viewable target when browser automation
1. Create a fresh tab and navigate. Prefer the harness's native/browser-canvas screenshot path before hand-rolling a Playwright/Puppeteer script; only fall back to a custom script when no native browser tool is exposed.
2. Preflight mutable injection by setting `document.title` and appending a `<script>` tag. Read-only evaluate APIs do not count.
3. If mutation is unavailable, skip live server, browser presentation, and injection; report fallback signal.
4. If mutation is available, start `node {{scripts_path}}/live-server.mjs --background`, present the browser if supported, label `[Human]`, scroll top, inject `http://localhost:PORT/detect.js`, wait 2-3 seconds, read `impeccable` console messages, then stop the live server.
4. If mutation is available, start `{{scripts_path}}/impeccable live-server --background`, present the browser if supported, label `[Human]`, scroll top, inject `http://localhost:PORT/detect.js`, wait 2-3 seconds, read `impeccable` console messages, then stop the live server.
5. For multi-view targets, inject on 3-5 representative pages.
<codex>
@@ -93,7 +93,7 @@ Codex Browser note: Use the Browser skill. Do not spend a Browser attempt on `fi
Return: CLI findings JSON/counts, browser console findings if applicable, false positives, and skipped/failed browser steps with concrete reasons.
After Assessment B returns usable CLI findings, reuse them. Do not rerun `detect.mjs` in the parent unless Assessment B failed, was truncated, or omitted count, rule names, or file locations.
After Assessment B returns usable CLI findings, reuse them. Do not rerun `impeccable detect` in the parent unless Assessment B failed, was truncated, or omitted count, rule names, or file locations.
<codex>
Codex failure accounting: final Run Notes must include target slug, ignore list, assessment independence, CLI detector, browser visibility, overlay injection, live-server cleanup, temp-file cleanup, and any fallback signal used. Do not run repo status checks, late API spelunking, or unrelated verification after the report is assembled.
@@ -229,7 +229,7 @@ Skip this step if the Setup slug was null (vague or root-level target).
2. **Pass the structured metadata** through `IMPECCABLE_CRITIQUE_META` (JSON), then run the write command:
```bash
IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"max_score":<n>,"na_heuristics":"<comma-separated numbers, or empty>","p0_count":<n>,"p1_count":<n>}' \
node {{scripts_path}}/critique-storage.mjs write "<resolved target>" <body-file>
{{scripts_path}}/impeccable critique-storage write "<resolved target>" <body-file>
```
`max_score` is the applicable maximum from the heuristic table (40 when every heuristic applied), so a later run can tell a renormalized total from a full one. For a local file target, the helper also records an exact content fingerprint so polish can distinguish the assessed bytes from later edits without relying on Git state or timestamps. The helper prints the absolute path it wrote. Leave that file on disk. Polish closes it; this run does not.
@@ -237,7 +237,7 @@ Skip this step if the Setup slug was null (vague or root-level target).
4. **Read the trend** for context:
```bash
node {{scripts_path}}/critique-storage.mjs trend "<resolved target>" 5
{{scripts_path}}/impeccable critique-storage trend "<resolved target>" 5
```
This returns a JSON array of the last 5 frontmatter entries (including the one you just wrote).
+4 -4
View File
@@ -6,14 +6,14 @@ This is maintenance, not design. Do not redesign anything, do not open files out
Three kinds of drift travel under "out of date". Keep them apart:
- **Tool version.** The installed skill is older than the published one. `context.mjs` reports that at boot as `UPDATE_AVAILABLE` and `npx impeccable update` fixes it. Not this command's job.
- **Tool version.** The installed skill is older than the published one. `impeccable context` reports that at boot as `UPDATE_AVAILABLE` and `npx impeccable update` fixes it. Not this command's job.
- **Schema drift.** An artifact was written by an older Impeccable: fields nothing reads, fields now expected, files in retired locations. Mechanical, and this command repairs most of it.
- **Truth drift.** The code moved on and the document no longer describes it. No file comparison settles this. `document` owns DESIGN.md, `init` owns PRODUCT.md, and this command's job is to hand them a specific gap rather than a vague suspicion.
## Step 1: Run the pass
```
node {{scripts_path}}/doctor.mjs --json
{{scripts_path}}/impeccable doctor --json
```
Add `--target <path>` when the user named a workspace, file, or route in a monorepo. Without it the report describes the repo root, and in a monorepo that is often the wrong project.
@@ -26,7 +26,7 @@ An empty `findings` array is the good outcome. Say so in one line and stop.
The severity says what should happen, not how bad it is.
- **`auto`** carries no decision. Run `node {{scripts_path}}/doctor.mjs --fix` once to apply these, then report what it moved in one line. Do not ask permission first, and do not ask about them afterward.
- **`auto`** carries no decision. Run `{{scripts_path}}/impeccable doctor --fix` once to apply these, then report what it moved in one line. Do not ask permission first, and do not ask about them afterward.
- **`mention`** needs the user to know but not to decide anything now. State each one in a sentence with its offered fix.
- **`route`** needs a specific command. Name the command and the gap it would close. Run it only if the user asks in this turn; `init` and `document` are conversations, not repairs you perform unattended.
@@ -51,4 +51,4 @@ The same restraint applies to `workspace-context-inherited`. Inheritance is a de
## Opting out of the boot check
`context.mjs` reports the cheap subset of these findings at session start, throttled to once a week per project. Set `"stalenessCheck": false` in `.impeccable/config.json` to silence that, or `IMPECCABLE_NO_STALENESS_CHECK=1` for one session. This command still works with the check disabled, and that is the combination to suggest for a user who wants the report only when they ask for it.
`impeccable context` reports the cheap subset of these findings at session start, throttled to once a week per project. Set `"stalenessCheck": false` in `.impeccable/config.json` to silence that, or `IMPECCABLE_NO_STALENESS_CHECK=1` for one session. This command still works with the check disabled, and that is the combination to suggest for a user who wants the report only when they ask for it.
+12 -12
View File
@@ -6,7 +6,7 @@ The hook runs the impeccable design detector on direct file edits to design-rele
The detector rules run in two tiers. The per-edit hook surfaces only the immediate tier: mechanical, unambiguous problems worth interrupting an edit for, such as broken images, overflowing or clipped content, contrast and legibility failures, gradient text, glow shadows, and design-system drift. Everything else (copy cadence, palette and typography taste, layout rhythm) is deferred to a deep pass on the `Stop` hook event, which runs the full rule set over every UI file touched in the session and surfaces the remaining findings once, deduplicated against what the per-edit pass already reported. A session with nothing left to report stops silently. Set `hook.perEditRules` to `"all"` in `.impeccable/config.json` to restore the full rule set on every edit. The Stop deep pass is wired for Claude Code, Codex, and Grok Build, which dispatch a native `Stop` hook event. Cursor does not get one (its stop hook is not consistently dispatched; the pre-write gate covers it), and GitHub Copilot's stop-style events do not feed context back to the model, so they keep the full detector per edit. Grok also fires an observe-only Stop with `reason: "shutdown"` after `end_turn`; skip that one, scan only `end_turn`.
Every hook is a mechanical pass. The reflexes no scanner catches live in [craft-floor.md](craft-floor.md), which the skill loads before it edits UI, so they apply whether or not a hook is wired. A session with no automatic hook gets one `MANUAL_DETECTOR_REQUIRED` directive from `context.mjs` asking for a single detector run at the end.
Every hook is a mechanical pass. The reflexes no scanner catches live in [craft-floor.md](craft-floor.md), which the skill loads before it edits UI, so they apply whether or not a hook is wired. A session with no automatic hook gets one `MANUAL_DETECTOR_REQUIRED` directive from `impeccable context` asking for a single detector run at the end.
This command toggles the hook **per project** by editing `.impeccable/config.json` (the unified Impeccable config; hook runtime settings live under its `hook` key, and shared detector ignores live under `detector`). Per-developer overrides, including the install consent decision (`hook.consent`) the CLI records, live in the gitignored `.impeccable/config.local.json`. Set `hook.enabled: false` to turn the hook off, `hook.quiet: true` to silence the clean/pending acks, or `hook.auditLog` to a file path for an NDJSON log. The legacy `IMPECCABLE_HOOK_DISABLED`, `IMPECCABLE_HOOK_QUIET`, and `IMPECCABLE_HOOK_LOG` env vars are still honored and override these config values when set.
@@ -40,7 +40,7 @@ The first argument is the action. Defaults to `status`.
2. Invoke the admin script and pass the user's output through verbatim:
```bash
node {{scripts_path}}/hook-admin.mjs <action> [args...]
{{scripts_path}}/impeccable hooks <action> [args...]
```
3. If `<action>` is `off`, follow up with a one-line note: "Done. New edits will not trigger the design hook in this project until you run `{{command_prefix}}impeccable hooks on`."
@@ -50,7 +50,7 @@ The first argument is the action. Defaults to `status`.
## Triage findings
The hook itself never writes ignore config; every exception goes through `hook-admin.mjs`. Triage each finding into one of three outcomes:
The hook itself never writes ignore config; every exception goes through `impeccable hooks`. Triage each finding into one of three outcomes:
- **Real design problem**: fix it. Never add an ignore to skip a fix or to push a blocked write through.
- **Confident false positive or sanctioned exception**: persist the narrowest ignore yourself and disclose it in your reply. The bar is evidence you can name: an intentional demo or fixture, documentation of bad design, literal or domain-appropriate motion (a ball that bounces), or a choice the user already confirmed. Put that evidence in `--reason` as `"<who decided: evidence>"`; write "user confirmed" only when the user actually did.
@@ -60,7 +60,7 @@ Self-serve stops at `ignore-value`. `ignore-file` and `ignore-rule` silence too
Prefer the narrowest exception:
- If the finding line shows an `ignore-value <rule> <value>` pair, pass it to `hook-admin.mjs ignore-value` with your `--reason`. This writes shared `.impeccable/config.json` by default.
- If the finding line shows an `ignore-value <rule> <value>` pair, pass it to `impeccable hooks ignore-value` with your `--reason`. This writes shared `.impeccable/config.json` by default.
- For value-specific findings such as `overused-font` and `bounce-easing`, use `ignore-value` for the specific value. Do not use `ignore-rule overused-font` for a specific font.
- If the finding has no value-specific command, such as `side-tab`, scope that one rule to the file: `ignore-value <id> "*" --file <path>`. Run `npx impeccable detect <path>` first to see what actually fires there.
- Reach for `ignore-file <path>` only when the whole file is out of scope for design review: a fixture, a generated artifact, a deliberate slop demo. It silences every rule for that file permanently, including rules that have not been written yet. A real UI surface with one noisy rule wants the file-scoped value ignore above.
@@ -70,42 +70,42 @@ Prefer the narrowest exception:
Example value-specific exception:
```bash
node {{scripts_path}}/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
{{scripts_path}}/impeccable hooks ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
```
Example self-served exception, with the evidence named:
```bash
node {{scripts_path}}/hook-admin.mjs ignore-value bounce-easing bounce-ball --shared --reason "Agent: literal ball-bounce animation, bounce easing is the subject"
{{scripts_path}}/impeccable hooks ignore-value bounce-easing bounce-ball --shared --reason "Agent: literal ball-bounce animation, bounce easing is the subject"
```
Example whole-rule font exception:
```bash
node {{scripts_path}}/hook-admin.mjs ignore-rule overused-font --all-values --reason "User asked to ignore overused fonts generally"
{{scripts_path}}/impeccable hooks ignore-rule overused-font --all-values --reason "User asked to ignore overused fonts generally"
```
Example one-rule-in-one-file exception, for a file that is still worth reviewing
for everything else:
```bash
node {{scripts_path}}/hook-admin.mjs ignore-value design-system-font-size "*" --file "src/overlay/widget.js" --reason "Injected widget builds its own type scale; DESIGN.md's ramp describes the site"
{{scripts_path}}/impeccable hooks ignore-value design-system-font-size "*" --file "src/overlay/widget.js" --reason "Injected widget builds its own type scale; DESIGN.md's ramp describes the site"
```
Example whole-file exception, for a file that is out of scope entirely:
```bash
node {{scripts_path}}/hook-admin.mjs ignore-file "src/legacy/Card.tsx"
{{scripts_path}}/impeccable hooks ignore-file "src/legacy/Card.tsx"
```
## Constraints
- Never modify `.impeccable/config.json` or `.impeccable/config.local.json` by hand from this command. Always go through `hook-admin.mjs` so writes stay validated and the file shape stays consistent. One exception: `detector.extensions` has no admin action, so when the user asks to cover a template stack, edit that one field in `.impeccable/config.json` directly and leave the rest of the file untouched.
- Do not edit the hook scripts themselves (`hook.mjs`, `hook-lib.mjs`, `hook-before-edit.mjs`) from this flow. Those are skill plumbing.
- Never modify `.impeccable/config.json` or `.impeccable/config.local.json` by hand from this command. Always go through `impeccable hooks` so writes stay validated and the file shape stays consistent. One exception: `detector.extensions` has no admin action, so when the user asks to cover a template stack, edit that one field in `.impeccable/config.json` directly and leave the rest of the file untouched.
- Do not edit the launcher or the binary behind `impeccable hook` and `impeccable hook-before-edit` from this flow. Those are skill plumbing.
- Cursor can block a proposed write when the detector finds a real issue. Claude Code, Codex, and GitHub Copilot do not block the edit; they emit a post-edit reminder instead. Disabling stops both blocking and reminders.
- The hook is bundled with the Impeccable skill and installed through project-local manifests: `.claude/settings.local.json`, `.codex/hooks.json`, `.cursor/hooks.json`, and `.github/hooks/impeccable.json`. On Codex, the user must approve the hook via `/hooks` the first time. On Cursor, confirm hooks are enabled under Settings -> Hooks. On GitHub Copilot, the CLI loads `.github/hooks/impeccable.json` once it is committed to the repository's default branch, and the cloud agent reads it from the repo directly.
## Failure modes
- If `.impeccable/config.json` or `.impeccable/config.local.json` is unreadable or malformed, the hook ignores that file and uses the remaining valid config/defaults. `hook-admin.mjs status` will show malformed files as ignored.
- If `.impeccable/config.json` or `.impeccable/config.local.json` is unreadable or malformed, the hook ignores that file and uses the remaining valid config/defaults. `impeccable hooks status` will show malformed files as ignored.
- If the user asks to "disable the hook" globally, lead with `{{command_prefix}}impeccable hooks off` (persistent for this project; writes `hook.enabled: false` to config). The legacy `IMPECCABLE_HOOK_DISABLED=1` env var also works as a one-shot override that follows the shell.
+4 -4
View File
@@ -4,7 +4,7 @@
## Step 1: Load current state
Use the PRODUCT.md path resolved by context.mjs. Update it instead of creating a competing authority. In a child app inheriting root context, confirm shared versus app-specific scope before writing.
Use the PRODUCT.md path resolved by `impeccable context`. Update it instead of creating a competing authority. In a child app inheriting root context, confirm shared versus app-specific scope before writing.
- **No PRODUCT.md:** explore, interview, and write it.
- **PRODUCT.md exists:** ask what product knowledge is stale or missing; do not reopen confirmed fields without a reason.
@@ -101,7 +101,7 @@ Platform is the bare value `web`, `ios`, `android`, or `adaptive`. Preserve usef
Copy the `impeccable:product-schema` comment verbatim, including when you update an older file. It records which version of the product record this file follows, so later versions can tell a deliberately short record from one written before a section existed, and never propose an interview the user has already sat through. Update the number only when this reference's template changes it. Sections a later version retires are reported to you at boot as deprecated; delete them when the user agrees rather than carrying them forward.
When the platform you just recorded is `ios`, `android`, or `adaptive`, load [ios.md](ios.md), [android.md](android.md), or both before any design work. On a project that had no PRODUCT.md, context.mjs could not know the platform and so never loaded them; init is the only place that learns the answer.
When the platform you just recorded is `ios`, `android`, or `adaptive`, load [ios.md](ios.md), [android.md](android.md), or both before any design work. On a project that had no PRODUCT.md, `impeccable context` could not know the platform and so never loaded them; init is the only place that learns the answer.
### Completion gate
@@ -109,7 +109,7 @@ Before loading new-work or resuming shape/build, verify that PRODUCT.md exists a
## Step 5: Record workflow defaults
When image generation is available and no `buildPath` is recorded yet, ask once how new surfaces should be built. Availability means a harness-native image tool or the API fallback that context.mjs reports as `IMAGE_GEN_AVAILABLE`, and the first of those leaves no trace in the boot output: context.mjs only sees the key, so a silent boot on a harness that generates images is not evidence there is nothing to ask about. This is its own question, never a clause riding inside another one. The stack round asks what to build with; this asks how the building starts, and an answer to the first carries no consent about the second. State the trade in the question the user actually reads, because the two names mean nothing to someone meeting them for the first time: **comp-first** (an image sets the bar before any code; bolder composition, slower, and the build must match the image) or **code-first** (build directly; the ambition is written into the direction contract and audited at the finish; leaner, faster).
When image generation is available and no `buildPath` is recorded yet, ask once how new surfaces should be built. Availability means a harness-native image tool or the API fallback that `impeccable context` reports as `IMAGE_GEN_AVAILABLE`, and the first of those leaves no trace in the boot output: `impeccable context` only sees the key, so a silent boot on a harness that generates images is not evidence there is nothing to ask about. This is its own question, never a clause riding inside another one. The stack round asks what to build with; this asks how the building starts, and an answer to the first carries no consent about the second. State the trade in the question the user actually reads, because the two names mean nothing to someone meeting them for the first time: **comp-first** (an image sets the bar before any code; bolder composition, slower, and the build must match the image) or **code-first** (build directly; the ambition is written into the direction contract and audited at the finish; leaner, faster).
Write the answer to `.impeccable/config.json` as `"buildPath": "comp"` or `"buildPath": "code"`, merging with the keys already there. Write only the value the user chose. A recommendation you made is not an answer you received, and a value taken from silence is a standing default nobody set: it then rides every future round in the project, which is the opposite of asking once. When the question goes unanswered, record nothing and say in one line which path this session is taking and that it is not stored. That path is comp-first, the default new-work applies wherever image generation exists and nothing is recorded; name it rather than choosing a quieter one, because a silent default invented here is the same failure as a value written without an answer. Unset is a working state, not a gap: the decision page's toggle governs each session, and new-work's one-time offer records the answer the first time the user flips it. The config is the only place this lives. It is a workflow setting, not product truth, so it never joins `## Stack` or any other PRODUCT.md section, where a second copy would outlive the setting and steer rounds nobody could trace back to it.
@@ -128,4 +128,4 @@ Recommend the next action from the actual project state:
- Existing surface needing work: name the most relevant scoped command.
- Web project ready for visual iteration: `/impeccable live` when configured.
If init was invoked by another request, resume without rerunning context.mjs; the native reference above is the one thing that run could not have given you, and new-work owns later visual decisions.
If init was invoked by another request, resume without rerunning `impeccable context`; the native reference above is the one thing that run could not have given you, and new-work owns later visual decisions.
+1 -1
View File
@@ -25,7 +25,7 @@ When a sub-agent tool is available and permitted, run these independently; other
2. **Mechanical scan:** run:
```bash
node {{scripts_path}}/detect.mjs --json --scope layout [target files or dirs]
{{scripts_path}}/impeccable detect --json --scope layout [target files or dirs]
```
Also inspect arbitrary spacing, overflow, stacking, and container behavior the detector cannot resolve. Keep mechanical evidence out of the first assessment, then synthesize both passes before editing. A clean scan cannot prove hierarchy or rhythm.
+6 -6
View File
@@ -1,4 +1,4 @@
One-time live-mode project setup. Loaded from [live.md](live.md) only when `live.mjs` reports `config_missing` / `config_invalid`, when `configDrift` needs handling, or when the config lacks `cspChecked`. Not part of the per-session hot path.
One-time live-mode project setup. Loaded from [live.md](live.md) only when `impeccable live` reports `config_missing` / `config_invalid`, when `configDrift` needs handling, or when the config lacks `cspChecked`. Not part of the per-session hot path.
## Write the config
@@ -32,9 +32,9 @@ Create the file at the `path` the boot reported (default `.impeccable/live/confi
| Astro | `[" <root layout .astro>"]` | `</body>` | `html` |
| Multi-page (separate HTML per route) | `["public/**/*.html"]` glob over the served dir | `</body>` | `html` |
Pick an anchor that exists in every file (`</body>` almost always works); `insertAfter` matches after a line instead. For multi-page sites prefer a glob so new pages are picked up automatically. For sites whose pages are rebuilt by a generator, the inject survives only until the next regeneration: re-run `live.mjs` after each build (accept is unaffected; it writes true source via the fallback flow).
Pick an anchor that exists in every file (`</body>` almost always works); `insertAfter` matches after a line instead. For multi-page sites prefer a glob so new pages are picked up automatically. For sites whose pages are rebuilt by a generator, the inject survives only until the next regeneration: re-run `impeccable live` after each build (accept is unaffected; it writes true source via the fallback flow).
**Framework adapters (auto-detected at inject time).** Every inject records what it wrote in `.impeccable/live/inject-journal.json`; the next inject or remove heals artifacts a crash or wrong-directory stop left behind. SvelteKit, Nuxt, and TanStack Start server-render their document shell, so a raw `<script>` in the entry template will not execute reliably; `live-inject.mjs` detects them and routes to a dedicated adapter (SvelteKit: dev-only root component from `+layout.svelte`; Nuxt: dev-only `.client.ts` plugin; TanStack Start: a generated dev-only `ImpeccableLiveRoot` component in `__root`). The `files` value stays a valid detection/CSP hint but is not the literal insertion site. A plain TanStack Router SPA takes the baseline Vite path.
**Framework adapters (auto-detected at inject time).** Every inject records what it wrote in `.impeccable/live/inject-journal.json`; the next inject or remove heals artifacts a crash or wrong-directory stop left behind. SvelteKit, Nuxt, and TanStack Start server-render their document shell, so a raw `<script>` in the entry template will not execute reliably; `impeccable live-inject` detects them and routes to a dedicated adapter (SvelteKit: dev-only root component from `+layout.svelte`; Nuxt: dev-only `.client.ts` plugin; TanStack Start: a generated dev-only `ImpeccableLiveRoot` component in `__root`). The `files` value stays a valid detection/CSP hint but is not the literal insertion site. A plain TanStack Router SPA takes the baseline Vite path.
## Config drift
@@ -45,7 +45,7 @@ On every boot the project is scanned for HTML files under common page roots (`pu
If `config.cspChecked === true`, skip this whole section; the user was already asked once.
```bash
node {{scripts_path}}/detect-csp.mjs
{{scripts_path}}/impeccable detect-csp
```
Output `{ shape, signals }`; the shape names the *patch mechanism*, so one template covers many frameworks:
@@ -97,6 +97,6 @@ Per-framework: Next.js inline `headers()` in `next.config.*`; Nuxt `routeRules['
## Troubleshooting
If the user said "no" to the CSP patch and later reports live not working: their dev CSP blocks `http://localhost:8400`. Delete `cspChecked` from `.impeccable/live/config.json` and re-run `live.mjs`; setup asks again.
If the user said "no" to the CSP patch and later reports live not working: their dev CSP blocks `http://localhost:8400`. Delete `cspChecked` from `.impeccable/live/config.json` and re-run `impeccable live`; setup asks again.
After setup, re-run `live.mjs`.
After setup, re-run `impeccable live`.
+29 -29
View File
@@ -12,13 +12,13 @@ Codex: run live helper commands, the app dev server, and any dependency-installi
Execute in order. No step skipped, no step reordered. Every tool output in live mode may carry an `_instructions` field: it is the authoritative next step for that exact situation, with real ids and paths substituted; when it conflicts with your recollection of this document, `_instructions` wins.
1. `live.mjs`: boot. If the request names or implies a file, route, or app inside a monorepo, infer the concrete path and run `node {{scripts_path}}/live.mjs --target <path>` instead; then run the rest of this live session from the returned `projectRoot`. The boot resolves the app root from dev-server config files and persists it in `.impeccable/live/roots.json`; every helper re-anchors to that manifest at startup (a wrong cwd cannot fork session state), PRODUCT.md / DESIGN.md are discovered upward to the git root, and relative helper args like `--file` resolve against the app root.
1. `impeccable live`: boot. If the request names or implies a file, route, or app inside a monorepo, infer the concrete path and run `{{scripts_path}}/impeccable live --target <path>` instead; then run the rest of this live session from the returned `projectRoot`. The boot resolves the app root from dev-server config files and persists it in `.impeccable/live/roots.json`; every helper re-anchors to that manifest at startup (a wrong cwd cannot fork session state), PRODUCT.md / DESIGN.md are discovered upward to the git root, and relative helper args like `--file` resolve against the app root.
2. Open the app URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). Never use `serverPort`; it's the helper, not the app. **Cursor:** `browser_navigate` to that URL before polling; do not skip. **Other harnesses:** use the available browser tool; if the URL is uncertain, ask the user once.
3. Poll loop with the default long timeout (600000 ms). Run `live-poll.mjs` again immediately after every event or `--reply`; Codex runs this one-shot poll in the foreground. Never pass a short `--timeout=`. The global bar's **Impeccable mark** dims with a pulsing amber dot when nothing is polling `/poll`; restart `live-poll.mjs` to reconnect.
3. Poll loop with the default long timeout (600000 ms). Run `impeccable live-poll` again immediately after every event or `--reply`; Codex runs this one-shot poll in the foreground. Never pass a short `--timeout=`. The global bar's **Impeccable mark** dims with a pulsing amber dot when nothing is polling `/poll`; restart `impeccable live-poll` to reconnect.
4. On `generate`: reuse `event.scaffold` when present; read the screenshot if present; load the action's reference; deliver variants; `--reply done`; poll again. Generate in this thread: you already hold the project's tokens and layout. The overlay preview IS the verification channel; do not screenshot, re-render, or QA variants between generate and accept. Apply craft-floor's contrast, spacing, and type floors by construction as you write; full verification runs once at accept on the chosen variant.
5. On `steer`: read the message and `pageUrl`; do the work; `--reply steer_done`; poll again. No pickup ack.
6. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges delivery, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts stay recoverable until `live-complete.mjs --id EVENT_ID` runs. Finish that cleanup before polling again.
7. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The journal under `.impeccable/live/sessions/` is canonical and replays unacknowledged work after a helper restart; the injected `live.js` re-attaches when the page reopens. Fall back to the direct-edit loop only when `live-resume.mjs` reports no active session, never because disconnects felt frequent.
6. On `accept` / `discard`: the poll script runs `impeccable live-accept`, acknowledges delivery, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts stay recoverable until `impeccable live-complete --id EVENT_ID` runs. Finish that cleanup before polling again.
7. If interrupted, run `impeccable live-status` or `impeccable live-resume` before guessing. The journal under `.impeccable/live/sessions/` is canonical and replays unacknowledged work after a helper restart; the injected `live.js` re-attaches when the page reopens. Fall back to the direct-edit loop only when `impeccable live-resume` reports no active session, never because disconnects felt frequent.
8. On `exit`: run the cleanup at the bottom.
Harness policy:
@@ -35,7 +35,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi
```
LOOP:
node {{scripts_path}}/live-poll.mjs # default long timeout; no --timeout=
{{scripts_path}}/impeccable live-poll # default long timeout; no --timeout=
Read JSON; dispatch on "type"
"generate" → Handle Generate; reply done; LOOP
@@ -56,10 +56,10 @@ LOOP:
## Start
```bash
node {{scripts_path}}/live.mjs
{{scripts_path}}/impeccable live
```
Output JSON: `{ ok, serverPort, serverToken, pageFiles, roots, hasProduct, product, productPath, hasDesign, design, designPath, hasSurfaceBrief, surfaceBrief }`. `roots` is the resolved root manifest; `projectRoot` mirrors `roots.appRoot`. The surface brief rides along; do not shell out to `surface-brief.mjs` separately. Precedence for generation: **DESIGN.md wins on visual decisions; PRODUCT.md wins on durable product and voice decisions; the surface brief wins on this surface's strategy.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components (Step 4 Phase A). Identity preservation is the default; departure requires the user's explicit redesign intent.
Output JSON: `{ ok, serverPort, serverToken, pageFiles, roots, hasProduct, product, productPath, hasDesign, design, designPath, hasSurfaceBrief, surfaceBrief }`. `roots` is the resolved root manifest; `projectRoot` mirrors `roots.appRoot`. The surface brief rides along; do not shell out to `impeccable surface-brief` separately. Precedence for generation: **DESIGN.md wins on visual decisions; PRODUCT.md wins on durable product and voice decisions; the surface brief wins on this surface's strategy.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components (Step 4 Phase A). Identity preservation is the default; departure requires the user's explicit redesign intent.
`serverPort`/`serverToken` belong to the small helper HTTP server (`/live.js`, SSE, `/poll`), not your dev server; the page URL is whatever origin serves a `pageFiles` entry.
@@ -70,12 +70,12 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`,
The append-only journal under `.impeccable/live/sessions/` is canonical durable state (not project source). When the chat was interrupted, polling was missed, the helper restarted, or the browser reloaded:
```bash
node {{scripts_path}}/live-status.mjs # helper state, active sessions, queued events; works with the helper down
node {{scripts_path}}/live-resume.mjs --id SESSION_ID # active snapshot, pending event, next safe action
node {{scripts_path}}/live-complete.mjs --id SESSION_ID # canonical manual final acknowledgement after verified cleanup
{{scripts_path}}/impeccable live-status # helper state, active sessions, queued events; works with the helper down
{{scripts_path}}/impeccable live-resume --id SESSION_ID # active snapshot, pending event, next safe action
{{scripts_path}}/impeccable live-complete --id SESSION_ID # canonical manual final acknowledgement after verified cleanup
```
Server restart rule: start `live-server.mjs` again, then poll; startup requeues unacknowledged events, so never ask the user to click Go again unless `live-resume.mjs` says no active session exists.
Server restart rule: start `impeccable live-server` again, then poll; startup requeues unacknowledged events, so never ask the user to click Go again unless `impeccable live-resume` says no active session exists.
## Handle `generate`
@@ -91,7 +91,7 @@ Speed matters; the user is watching the selected element. Reuse preflight metada
2. If `event.scaffold` is present, use it and do **not** run the helper again. Otherwise:
```bash
node {{scripts_path}}/live-insert.mjs --id EVENT_ID --count EVENT_COUNT --position after \
{{scripts_path}}/impeccable live-insert --id EVENT_ID --count EVENT_COUNT --position after \
--element-id "ANCHOR_ID" --classes "class1,class2" --tag "section" --text "ANCHOR_TEXT"
```
@@ -112,14 +112,14 @@ When `event.scaffold` is present, the helper already found the source and comput
**On source-preview targets `event.scaffold` carries `sourceWritten: false`.** The helper did NOT write the wrapper; it hands you `scaffold.wrapperBlock` plus the picked element's source range (`replaceStartLine`, `replaceEndLine`, 1-indexed). Write the wrapper **and** all variants in ONE edit: splice your variants into `wrapperBlock` at the "Variants: insert below this line" marker, then replace lines `[replaceStartLine, replaceEndLine]` with the result. A separate scaffold write reloads the framework before your variant write lands and strands the browser at 0/N. (`replaceEndLine < replaceStartLine` means insert mode: insert, remove nothing.) The `svelte-component` path never sets `sourceWritten`.
```bash
node {{scripts_path}}/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" --text "TEXT_SNIPPET"
{{scripts_path}}/impeccable live-wrap --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" --text "TEXT_SNIPPET"
```
Flag mapping (keep separate, never collapse into `--query`): `--element-id``event.element.id`; `--classes` ← classes joined with commas; `--tag` ← tagName; `--text` ← first ~80 chars of textContent, **every call**: it disambiguates repeated sibling components, without it wrap lands on the first match. If `event.pageUrl` implies the file, pass `--file PATH`. If `--text` still matches several candidates, wrap exits `{ error: "element_ambiguous", candidates, fallback: "agent-driven" }`: pick the right range from page context and write the wrapper manually per the fallback flow.
Success output: `{ file, insertLine, commentSyntax, styleMode, styleTag, cssSelectorPrefixExamples, cssAuthoring }` (plus the `sourceWritten: false` fields above on source-preview targets). Run directly with no preflight scaffold, it writes the wrapper itself and you splice variants at `insertLine`. `styleMode` controls how preview CSS must be authored. Treat it as a detected capability mode, not a framework guess: `scoped` means `@scope ([data-impeccable-variant="N"])` rules; `astro-global-prefixed` means explicit `[data-impeccable-variant="N"]` prefixes with the exact returned `styleTag`. Use `cssAuthoring` as the source of truth for the current file (styleTag, selector strategy, requirements, forbidden patterns); apply no framework-specific exception unless it says to.
For Svelte/SvelteKit targets, `live-wrap.mjs` returns `previewMode: "svelte-component"` with `file` pointing at a temporary `node_modules/.impeccable-live/<id>/manifest.json`, `componentDir` holding the variant components, and `sourceFile` the real route. The scaffold is AST-based: control-flow blocks (`{#each}`, `{#if}`) survive intact and a free each-collection crosses the contract as ONE structured prop (kind `collection`). The payload includes `componentStubMarkup` (the prop-substituted markup already written into every stub), so do not read the manifest or stubs back. EDIT `v1.svelte`, `v2.svelte`, ... in place; never delete and recreate them; keep the stub's control flow and `propContract` prop names; never flatten a loop into literal items. The stub `<style>` arrives seeded with the source rules that currently style the selection; restyle or delete them freely. On accept, any seeded rule your variant does not re-declare is REMOVED from the source (the preview never applied it, so the user approved a design without it). Use semantic class selectors, no `@scope`, no `data-impeccable-*`. Reply with `--file` set to the manifest path; the browser mounts the compiled components so Svelte HMR does not reset page state. Accept merges the chosen component back mechanically (markup restored to route expressions, CSS reconciled, params baked, indentation preserved); you have no post-accept cleanup on this path. When the selection contains constructs a detached preview cannot support (component tags, `bind:`/`use:`, await blocks, inline scripts, spread attributes), wrap returns the normal source-preview wrapper with `previewFallback: { from: "svelte-component", reason }`; just follow the returned shape.
For Svelte/SvelteKit targets, `impeccable live-wrap` returns `previewMode: "svelte-component"` with `file` pointing at a temporary `node_modules/.impeccable-live/<id>/manifest.json`, `componentDir` holding the variant components, and `sourceFile` the real route. The scaffold is AST-based: control-flow blocks (`{#each}`, `{#if}`) survive intact and a free each-collection crosses the contract as ONE structured prop (kind `collection`). The payload includes `componentStubMarkup` (the prop-substituted markup already written into every stub), so do not read the manifest or stubs back. EDIT `v1.svelte`, `v2.svelte`, ... in place; never delete and recreate them; keep the stub's control flow and `propContract` prop names; never flatten a loop into literal items. The stub `<style>` arrives seeded with the source rules that currently style the selection; restyle or delete them freely. On accept, any seeded rule your variant does not re-declare is REMOVED from the source (the preview never applied it, so the user approved a design without it). Use semantic class selectors, no `@scope`, no `data-impeccable-*`. Reply with `--file` set to the manifest path; the browser mounts the compiled components so Svelte HMR does not reset page state. Accept merges the chosen component back mechanically (markup restored to route expressions, CSS reconciled, params baked, indentation preserved); you have no post-accept cleanup on this path. When the selection contains constructs a detached preview cannot support (component tags, `bind:`/`use:`, await blocks, inline scripts, spread attributes), wrap returns the normal source-preview wrapper with `previewFallback: { from: "svelte-component", reason }`; just follow the returned shape.
**Params on component-preview paths go in a sidecar, never as an attribute** (Svelte parses `{` in attribute values as an expression). Declare them in `componentDir/params.json` keyed by variant number, using the schema from section 7:
@@ -239,39 +239,39 @@ Budget scales with the element's VISUAL weight (count visual children, not DOM d
Three kinds: `range` (slider; drives `--p-<id>`; author `var(--p-color-amount, 0.5)`; fields min/max/step/default/label), `steps` (segmented radio; drives `data-p-<id>`; author `:scope[data-p-density="airy"] .grid { ... }`; fields options/default/label), `toggle` (drives both `--p-<id>: 0|1` and attribute presence; fields default/label). Reset on variant switch is a known limitation: each variant starts at its declared defaults.
**On accept**, the browser sends current values and `live-accept.mjs` writes them as a sibling comment: `<!-- impeccable-param-values SESSION_ID: {"color-amount":0.7} -->`. Carbonize cleanup bakes them: keep only the matching `steps`/`toggle` branch, drop the others, collapse `:scope[data-p-…]` to semantic rules; substitute `range` literals or update the var's default.
**On accept**, the browser sends current values and `impeccable live-accept` writes them as a sibling comment: `<!-- impeccable-param-values SESSION_ID: {"color-amount":0.7} -->`. Carbonize cleanup bakes them: keep only the matching `steps`/`toggle` branch, drop the others, collapse `:scope[data-p-…]` to semantic rules; substitute `range` literals or update the var's default.
### 8. Signal done
```bash
node {{scripts_path}}/live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH
{{scripts_path}}/impeccable live-poll --reply EVENT_ID done --file RELATIVE_PATH
```
`RELATIVE_PATH` is relative to project root; the browser fetches source directly if the dev server lacks HMR. Then poll again immediately.
### Aborting an in-flight session
If wrap or generation fails after the browser flipped to GENERATING, tell the **browser** so its bar resets: `node {{scripts_path}}/live-poll.mjs --reply EVENT_ID error "Short reason"`. Never use `live-accept --discard` for this (pure file mutator, browser never sees it, bar sticks on dots); `--discard` is only source-side cleanup for a discard the browser itself initiated.
If wrap or generation fails after the browser flipped to GENERATING, tell the **browser** so its bar resets: `{{scripts_path}}/impeccable live-poll --reply EVENT_ID error "Short reason"`. Never use `live-accept --discard` for this (pure file mutator, browser never sees it, bar sticks on dots); `--discard` is only source-side cleanup for a discard the browser itself initiated.
## Handle fallback
When wrap returns `fallback: "agent-driven"`, you pick the source file yourself; the goal is unchanged: three preview variants now, and the accepted one persisted where the next build cannot wipe it.
1. **Find where the element really lives** from the error payload: `element_not_in_source` + `generatedMatch` means the served HTML is generated, so find the generator's template or partial; `element_not_found` means runtime-injected, so find the rendering component or data source; `file_is_generated` resolves the same way. A purely visual change may belong in a shared stylesheet rather than a template.
2. **Preview in the served file**: manually write the same wrapper scaffold `live-wrap.mjs` produces (`<!-- impeccable-variants-start ID --><div data-impeccable-variants="ID" data-impeccable-variant-count="3" style="display: contents">…</div><!-- end -->`) into the file the browser actually loaded, insert your variant divs, `--reply EVENT_ID done --file <served file>`. This edit is temporary; a regen wiping it is fine.
2. **Preview in the served file**: manually write the same wrapper scaffold `impeccable live-wrap` produces (`<!-- impeccable-variants-start ID --><div data-impeccable-variants="ID" data-impeccable-variant-count="3" style="display: contents">…</div><!-- end -->`) into the file the browser actually loaded, insert your variant divs, `--reply EVENT_ID done --file <served file>`. This edit is temporary; a regen wiping it is fine.
3. **On accept, write to true source** (accept refuses generated files, so `_acceptResult.handled` is usually `false` here): structural change → template/component source; visual-only → the right stylesheet; content rendered from data → the data source or render logic. Then remove the temporary wrapper from the served file.
4. **On discard**, just remove the temporary wrapper.
## Handle `accept`
Event: `{id, variantId, _acceptResult, _completionAck}`. The poll script already ran `live-accept.mjs` deterministically and acknowledged delivery; the browser DOM is already updated.
Event: `{id, variantId, _acceptResult, _completionAck}`. The poll script already ran `impeccable live-accept` deterministically and acknowledged delivery; the browser DOM is already updated.
- The accept event includes `pageUrl`; the poll script must forward it to `live-accept.mjs --page-url PAGE_URL` so accept-time cleanup only scrubs staged copy edits for the current page.
- `_completionAck.ok !== true`: do not poll yet. Run `live-status.mjs` / `live-resume.mjs`, finish cleanup manually if needed, then `live-complete.mjs --id EVENT_ID`.
- The accept event includes `pageUrl`; the poll script must forward it to `impeccable live-accept --page-url PAGE_URL` so accept-time cleanup only scrubs staged copy edits for the current page.
- `_completionAck.ok !== true`: do not poll yet. Run `impeccable live-status` / `impeccable live-resume`, finish cleanup manually if needed, then `impeccable live-complete --id EVENT_ID`.
- `handled: true, carbonize: false`: nothing to do; poll again.
- `handled: true, carbonize: true`: required cleanup below; `_acceptResult.todo`, `_completionAck.requiresComplete`, and the stderr banner all point at it.
- `handled: false, mode: "fallback"`: the session lived in a generated file; you already wrote true source in fallback Step 3; clean the temporary wrapper and poll.
- `handled: false, mode: "error"`: **do not hand-edit the file.** `source_locked`: rerun the same `live-accept.mjs` command (idempotent) until the publisher releases. `accept_receipt_conflict`: the session already resolved as `priorOperation`; run `live-status.mjs` and tell the user. Anything else: report briefly, run `live-status.mjs` first.
- `handled: false, mode: "error"`: **do not hand-edit the file.** `source_locked`: rerun the same `impeccable live-accept` command (idempotent) until the publisher releases. `accept_receipt_conflict`: the session already resolved as `priorOperation`; run `impeccable live-status` and tell the user. Anything else: report briefly, run `impeccable live-status` first.
- `handled: false` without `mode`: manual cleanup: read file, find markers, edit.
### Required after accept (carbonize)
@@ -284,15 +284,15 @@ Event: `{id, variantId, _acceptResult, _completionAck}`. The poll script already
4. **Unwrap the accepted content**: delete the inner variant div (and on JSX the outer `data-impeccable-carbonize` div); drop `data-impeccable-params` and all `data-p-*` attributes.
5. **Delete** the inline `<style>` block, the param-values comment, both carbonize markers, and any `@scope` rules for non-accepted variants.
Then run `live-complete.mjs --id SESSION_ID` and verify `phase: "completed"` before polling again. The command is a gate, not a formality: it refuses with `error: "source_dirty"` plus findings while any live-mode leftover remains; fix and rerun (`--force` only for false positives).
Then run `impeccable live-complete --id SESSION_ID` and verify `phase: "completed"` before polling again. The command is a gate, not a formality: it refuses with `error: "source_dirty"` plus findings while any live-mode leftover remains; fix and rerun (`--force` only for false positives).
## Handle `discard`
Event: `{id, _acceptResult, _completionAck}`. The poll script already restored the original and acknowledged `discarded`. Nothing to do unless `_completionAck.ok !== true`; then `live-complete.mjs --id EVENT_ID --discarded` and poll again.
Event: `{id, _acceptResult, _completionAck}`. The poll script already restored the original and acknowledged `discarded`. Nothing to do unless `_completionAck.ok !== true`; then `impeccable live-complete --id EVENT_ID --discarded` and poll again.
## Handle `steer`
Event: `{id, message, pageUrl}`: page-level direction from the global bar's Steer control (typed or spoken), no element context, no variant cycling. Read `message`, inspect the page or files as needed, make edits or answer in prose. Reply `node {{scripts_path}}/live-poll.mjs --reply EVENT_ID steer_done ["Optional short toast"]`, or on failure `--reply EVENT_ID error "Short reason"`, then poll immediately. No separate pickup reply; the Steer bar unlocks on `steer_done` or `error`.
Event: `{id, message, pageUrl}`: page-level direction from the global bar's Steer control (typed or spoken), no element context, no variant cycling. Read `message`, inspect the page or files as needed, make edits or answer in prose. Reply `{{scripts_path}}/impeccable live-poll --reply EVENT_ID steer_done ["Optional short toast"]`, or on failure `--reply EVENT_ID error "Short reason"`, then poll immediately. No separate pickup reply; the Steer bar unlocks on `steer_done` or `error`.
## Handle `prefetch`
@@ -308,7 +308,7 @@ When native subagents are available, delegate source edits to `impeccable_manual
If `repair` is present, the previous Apply changed source but final validation failed. Fix the current source and return the same canonical JSON result; do not roll files back yourself. The browser will ask the user before any rollback.
After source edits finish, reply exactly once with `node {{scripts_path}}/live-poll.mjs --reply EVENT_ID done --data '{"status":"done","appliedEntryIds":["8hexid"],"failed":[],"files":["src/page.html"],"notes":[]}'`. Use `status:"partial"` or `status:"error"` with `failed[]` when not every entry applied. Then poll again. Never reply without the event id; `--reply done --file ...` is invalid for manual Apply.
After source edits finish, reply exactly once with `{{scripts_path}}/impeccable live-poll --reply EVENT_ID done --data '{"status":"done","appliedEntryIds":["8hexid"],"failed":[],"files":["src/page.html"],"notes":[]}'`. Use `status:"partial"` or `status:"error"` with `failed[]` when not every entry applied. Then poll again. Never reply without the event id; `--reply done --file ...` is invalid for manual Apply.
## Exit
@@ -317,11 +317,11 @@ The user stops live mode by saying so in chat, closing the tab (SSE drops; poll
## Cleanup
```bash
node {{scripts_path}}/live-server.mjs stop
{{scripts_path}}/impeccable live-server stop
```
Stops the helper and runs `live-inject.mjs --remove` to strip the injected script (use `stop --keep-inject` to keep it for a quick restart; `.impeccable/live/config.json` persists as project config). Then search for and remove any leftover `impeccable-variants-start` wrappers and `impeccable-carbonize-start` blocks.
Stops the helper and runs `impeccable live-inject --remove` to strip the injected script (use `stop --keep-inject` to keep it for a quick restart; `.impeccable/live/config.json` persists as project config). Then search for and remove any leftover `impeccable-variants-start` wrappers and `impeccable-carbonize-start` blocks.
## First-time setup
Only when `live.mjs` reports `config_missing` / `config_invalid`, or `configDrift` needs explaining, or the config lacks `cspChecked`: read [live-setup.md](live-setup.md). It owns the config schema, the per-framework `files` table, injection adapters, drift healing, and the CSP detection and consent flow.
Only when `impeccable live` reports `config_missing` / `config_invalid`, or `configDrift` needs explaining, or the config lacks `cspChecked`: read [live-setup.md](live-setup.md). It owns the config schema, the per-framework `files` table, injection adapters, drift healing, and the CSP detection and consent flow.
+15 -15
View File
@@ -34,19 +34,19 @@ Inherit its world and composition. Resolve only the new purpose, content, hierar
Keep the visual system fixed. Derive five to seven materially different structures from the content, task, and user behavior, ordered by resonance. For a genuinely open whole page, screen, or flow, run:
`node {{scripts_path}}/concept-seed.mjs --scope surface --mode <mode>`
`{{scripts_path}}/impeccable concept-seed --scope surface --mode <mode>`
The script deals three of your structures; the dice pick which three reach the user, breaking the ranking rut while the user keeps a real choice. Present them on the decision page as full cards of equal salience, the dealt lead under kicker THE ROLL, with steer and re-roll; the user locks one. No canon card and no pick card at surface scope: the world is settled, so every card visualizes composition, not identity. With image generation and a comp-led default (`.impeccable/config.json`; the build-path paragraph below), each card declares a `comp` under `.impeccable/mocks/decision/`, generated after serving, in reading order, under [visualize.md](visualize.md)'s comp discipline. Anchor each comp on the established identity: pass a screenshot of a representative existing page as a reference image (the harness image tool's input image, or `generate-image.mjs --ref`) with a prompt that leads with the new surface's structure and names DESIGN.md's palette, type, and component character; prose paraphrases of a design system drift, pixel references do not. Without image generation, or under a code-led default, each card carries a `wireframe` schematic (`serve-question.mjs --schema`) the page draws itself. Locking a card is the approval and sets the build path: a locked comp builds comp-led with that comp as the approved comp, discharging [visualize.md](visualize.md)'s three-option round with no second approval point; a locked wireframe builds code-led, its ambition carried by the direction contract. Never run the script for a local extension or a precisely specified narrow request; shape those directly.
The script deals three of your structures; the dice pick which three reach the user, breaking the ranking rut while the user keeps a real choice. Present them on the decision page as full cards of equal salience, the dealt lead under kicker THE ROLL, with steer and re-roll; the user locks one. No canon card and no pick card at surface scope: the world is settled, so every card visualizes composition, not identity. With image generation and a comp-led default (`.impeccable/config.json`; the build-path paragraph below), each card declares a `comp` under `.impeccable/mocks/decision/`, generated after serving, in reading order, under [visualize.md](visualize.md)'s comp discipline. Anchor each comp on the established identity: pass a screenshot of a representative existing page as a reference image (the harness image tool's input image, or `impeccable generate-image --ref`) with a prompt that leads with the new surface's structure and names DESIGN.md's palette, type, and component character; prose paraphrases of a design system drift, pixel references do not. Without image generation, or under a code-led default, each card carries a `wireframe` schematic (`impeccable serve-question --schema`) the page draws itself. Locking a card is the approval and sets the build path: a locked comp builds comp-led with that comp as the approved comp, discharging [visualize.md](visualize.md)'s three-option round with no second approval point; a locked wireframe builds code-led, its ambition carried by the direction contract. Never run the script for a local extension or a precisely specified narrow request; shape those directly.
### Create or replace the visual world
1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; both are the rut, kept out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world.
2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily. A nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families.
3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience.
4. Run `node {{scripts_path}}/concept-seed.mjs --scope direction --mode <mode>` and follow what it prints. No substitute, no skip: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure; the roll is what keeps every run from converging on the category default. The script assigns the direction to build and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture list is the point. Close with a verdict per challenger, decided before any borrowing: wins (beats the assigned direction on both axes; becomes the build candidate), competitive (holds one axis; stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a lifted motif is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. <!-- rule:skill-concept-procedure --> <!-- rule:skill-verdict-and-donation -->
4. Run `{{scripts_path}}/impeccable concept-seed --scope direction --mode <mode>` and follow what it prints. No substitute, no skip: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure; the roll is what keeps every run from converging on the category default. The script assigns the direction to build and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture list is the point. Close with a verdict per challenger, decided before any borrowing: wins (beats the assigned direction on both axes; becomes the build candidate), competitive (holds one axis; stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a lifted motif is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. <!-- rule:skill-concept-procedure --> <!-- rule:skill-verdict-and-donation -->
5. Present one direction, fully committed and already raised by the hand it beat, raises visible as named lines: world, first viewport, visitor path, signature interaction, cross-surface reach, honest risk. Route each challenger by verdict: winning and competitive challengers are full alternates with their QUALITY BAR cards and one-line case; declined challengers render demoted, compact and quiet, each carrying its verdict and what the direction kept from it, never full-size, never silently dropped, still adoptable on request. The verdict informs the user's choice, never pre-empts it; the demoted row is the hand's proof of judgment. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker IMPECCABLES PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often where most runs in this category land, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: a lineup of your candidates hands selection back to a taste function and invites the safest card. The pick never takes the lead position; when the dice assign your top candidate there is no pick card, and the assigned card notes it topped your list. Add re-roll with an optional one-line steer, in three registers: plain (a fresh hand, same spread), safer (your remaining conventional grounded candidates plus the canon against named competitors), bolder (foreign forms only, at full commitment). The register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register <value>` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool, whose option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit last; declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel. <!-- rule:skill-pick-card-one-only -->
The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it (the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path), convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. Record a standing preference as a brand commitment in PRODUCT.md. <!-- rule:skill-canon-standing-exit --> Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. Re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Resolve collisions field by field: preserve every user- or brief-pinned constraint. In dimensions the brief leaves open, the assignment still binds through its topology, controls, state vocabulary, and ritual; when only its materials conflict with a pinned visual direction or PRODUCT.md brand commitment, translate that material expression and name the translation in the presented direction. A look mismatch is not grounds to re-roll. <!-- rule:skill-assigned-plus-reroll --> Present the decision visually: write an options payload with the assigned direction leading, its raised lines included; the pick card when one exists; the dealt challengers as alternates with their QUALITY BAR cards, verdicts, and kept lines; re-roll with its safer and bolder registers; steer; canon enabled; and `buildPath` carrying the recorded default with `toggle: true` whenever image generation exists (details in the build-path paragraph below). A degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy: thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (`--schema` prints the exact shape); the page renders identity from these fields, demotes declined challengers to their row on its own, and a challenger's catalog image rides as labeled inspiration, never the promise of the build. Author `canonCard` too: the category standard as one honest card, same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node {{scripts_path}}/serve-question.mjs --start --payload <file>` (`--schema` first for the payload shape). It daemonizes, prints the page URL and a key, and exits; open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key <key>`, repeating while it exits 3; the ANSWER prints as JSON. An ANSWER of `{"optionId":"reroll"}` keeps the server alive and the page open on a loading hand: rerun concept-seed with the same `--scope` and `--mode` plus `--from <seed-key> --reroll <n>` (1 on the first re-roll, counting up), build the next payload, deliver it with `--update --key <same key> --payload <file>`, then return to `--wait` on that key. Never `--start` a second server or fall back to chat here: either strands the open tab on a hand that never arrives. Exit 4 means the page closed unanswered: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may run the script without `--start` and let it auto-open and block. Never predict the fallback: run the script, and only exit code 2 from starting it routes the decision to the structured tool; that exit is the fallback, never an error to retry. <!-- rule:skill-visual-decision-page -->
The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it (the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path), convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. Record a standing preference as a brand commitment in PRODUCT.md. <!-- rule:skill-canon-standing-exit --> Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. Re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Resolve collisions field by field: preserve every user- or brief-pinned constraint. In dimensions the brief leaves open, the assignment still binds through its topology, controls, state vocabulary, and ritual; when only its materials conflict with a pinned visual direction or PRODUCT.md brand commitment, translate that material expression and name the translation in the presented direction. A look mismatch is not grounds to re-roll. <!-- rule:skill-assigned-plus-reroll --> Present the decision visually: write an options payload with the assigned direction leading, its raised lines included; the pick card when one exists; the dealt challengers as alternates with their QUALITY BAR cards, verdicts, and kept lines; re-roll with its safer and bolder registers; steer; canon enabled; and `buildPath` carrying the recorded default with `toggle: true` whenever image generation exists (details in the build-path paragraph below). A degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy: thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (`--schema` prints the exact shape); the page renders identity from these fields, demotes declined challengers to their row on its own, and a challenger's catalog image rides as labeled inspiration, never the promise of the build. Author `canonCard` too: the category standard as one honest card, same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `{{scripts_path}}/impeccable serve-question --start --payload <file>` (`--schema` first for the payload shape). It daemonizes, prints the page URL and a key, and exits; open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key <key>`, repeating while it exits 3; the ANSWER prints as JSON. An ANSWER of `{"optionId":"reroll"}` keeps the server alive and the page open on a loading hand: rerun concept-seed with the same `--scope` and `--mode` plus `--from <seed-key> --reroll <n>` (1 on the first re-roll, counting up), build the next payload, deliver it with `--update --key <same key> --payload <file>`, then return to `--wait` on that key. Never `--start` a second server or fall back to chat here: either strands the open tab on a hand that never arrives. Exit 4 means the page closed unanswered: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may run the script without `--start` and let it auto-open and block. Never predict the fallback: run the script, and only exit code 2 from starting it routes the decision to the structured tool; that exit is the fallback, never an error to retry. <!-- rule:skill-visual-decision-page -->
When image generation exists, every card also declares a `comp` path under `.impeccable/mocks/decision/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the comps; the page shimmer-waits per slot and the user may answer before they land. Each card's image is that direction's north-star comp at full fidelity under [visualize.md](visualize.md)'s comp discipline: the requested surface's first viewport, structure-led prompt, real product name and real content, no invented commercial claims, in that card's own palette, type character, and material world, committed all the way. Generation takes the same time at any fidelity, so an unfinished draft pays comp cost for draft quality; fairness between cards is equal fidelity in each card's own grammar, one surface, one aspect, never shared unfinishedness. The frame's aspect is the surface's own: portrait at device viewport for a native app or mobile-first surface, landscape for desktop web; the decision page adapts to either, and a phone screen comped landscape is a broken frame, not a neutral default. Produce in reading order, the assigned card, then the pick, then the full-card hand, then canon, each file written with its prompt sidecar the moment it is done, so a re-roll's spend front-loads onto the cards read first; declined challengers get no comp, their catalog thumb is their face. With parallel subagents, fan out one agent per card: each spawn is the shipped asset producer with a single-comp packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight. Regenerate inline any slot still empty when its agent returns; drop without ceremony any slot still empty when the user answers. No other supervision is owed. Without parallel subagents, generate in the main thread after serving, same order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. The chosen card's comp is not spent by the choice: comp-led, it enters the comp round as compositional option one; code-led, it returns at the finish review as the critique reference, what the image dared that the build did not. Unchosen comps stay in `.impeccable/mocks/decision/` as the round's spent hand; they carry no approval and imply none. With no image generation, cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. <!-- rule:skill-decision-comps-full-fidelity --> <!-- rule:skill-salience-parity -->
@@ -80,15 +80,15 @@ On a new or replacement world, DESIGN.md is written at finish, from the built wo
Read the existing surface brief before updating it:
`node {{scripts_path}}/surface-brief.mjs read <primary-target>`
`{{scripts_path}}/impeccable surface-brief read <primary-target>`
`node {{scripts_path}}/surface-brief.mjs write <primary-target> <body-file> [related-target ...]`
`{{scripts_path}}/impeccable surface-brief write <primary-target> <body-file> [related-target ...]`
After writing, read the brief once more and verify that all six contract blocks and the seed key are present before building.
Keep the brief small: scope and visitor mode; audience, job, action/task, proof/content, and constraints; chosen direction and memorable moment; unresolved decisions. Do not copy global product truth or DESIGN.md tokens into it.
On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback context.mjs reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. <!-- rule:skill-visualize-before-build -->
On a comp-led build, whenever any image generation is available (a harness-native tool or the API fallback `impeccable context` reports), the locked direction is visualized before it is built, never skipped: load [visualize.md](visualize.md) and follow it, three compositional options put before the user for approval, the chosen card's decision comp plus two variations. This step is proven to produce the most compositional and ambitious work. On a code-led build the comp round is skipped by contract, never by drift: the ambition it would have carried lives in the direction contract's FIRST VIEWPORT block and named signature interaction, and the finish reviewer audits those promises in behavior. <!-- rule:skill-visualize-before-build -->
For `shape`, return the selected direction to [shape.md](shape.md) and stop before persistence or implementation.
@@ -100,16 +100,16 @@ Build the assigned direction, not a safer interpretation of it. The form supplie
When an approved comp exists, it is a spatial contract, not a mood board: only the user can downgrade its authority, in explicit words. Models systematically believe their HTML, CSS, and SVG recreation of an image succeeded when it did not, so the build runs as a state machine on disk whose gates measure the screen against the comp instead of asking you to remember it. Start it once, and let it tell you what is next:
`node {{scripts_path}}/build-phase.mjs start --direction <seed key> --kind <assigned|pick|challenger|canon>` right after the direction choice (this is also the choice ping; the roll's output names the exact command), or `start --comp <approved comp>` when a surface round already locked one.
`{{scripts_path}}/impeccable build-phase start --direction <seed key> --kind <assigned|pick|challenger|canon>` right after the direction choice (this is also the choice ping; the roll's output names the exact command), or `start --comp <approved comp>` when a surface round already locked one.
Then, in order, each closed by `node {{scripts_path}}/build-phase.mjs advance` (every script below lives under `{{scripts_path}}/` and runs with `node`; exit 2 means the gate failed and printed why; fix that and advance again; write nothing for a later phase while an earlier gate is open):
Then, in order, each closed by `{{scripts_path}}/impeccable build-phase advance` (every verb below runs as `{{scripts_path}}/impeccable <verb>`; exit 2 means the gate failed and printed why; fix that and advance again; write nothing for a later phase while an earlier gate is open):
0. **comps.** The comp round from [visualize.md](visualize.md): three compositional comps of the requested surface at its own viewport under `.impeccable/mocks/`, each with a prompt sidecar, put in front of the user; the chosen one's sidecar gets `"approved": true`. The gate counts them and reads the approval; a `start --comp` skips this phase because it already happened.
The comp-led path is a frontier-tier job: it asks the builder to hold a measured layout, place plates at their boxes, and act on numeric readings across a dozen attempts. Smaller or faster models produce a recognisable page and stall under the hero gate; if the model in hand is one of those, say so before the direction round and take the code-led path, or expect the run to end at the hero with its readings unmet.
1. **spec.** Measure the comp: `comp-spec.mjs --comp <comp> --grid` writes a coordinate grid over the comp; open it, name every salient region by grid span in a regions file (text and control regions snap to the largest ink mass inside their span, so a headline named B1:E4 measures as the headline and not the column beside it; `snap: false` keeps the span, and an explicit `box` is taken as drawn) (kind `plate` / `image` / `texture` for anything painted: every illustration, photograph, figure, product object, and material texture; `text` / `control` / `chrome` for what code draws; every region carries a `note` saying what the comp shows there, which the plate prompt and the gate messages read), and run `comp-spec.mjs --comp <comp> --regions <file>`. The spec carries each region's box, sampled palette, and medium; `comp-spec.mjs --print` is the build's reference from here on. Type is measured, not guessed: `font-match.mjs --measure <text region>` reads the comp's cap height, width class, and weight off the pixels, and `font-match.mjs --rank <region> --text "..."` takes its candidates from a fingerprint index of the Google Fonts catalog (the nearest faces to the crop's shape) plus any names you pass with `--candidates`, renders them at that cap height with the region's words, and ranks them by fingerprint distance (its `USE` line is the CSS; its proof sheet shows the comp over the top three); with no browser resolvable it records the catalog's nearest face and says the size is estimated, which is still the choice to build on. Do not install a browser to rank, and never write a `chosen` face into the spec by hand: the gate accepts only what font-match wrote. The spec gate refuses to close until the lead text region is measured and ranked. A region note that describes painted material (a diagram, drawing, photograph, texture) under a code kind is refused at the spec: reclassify it as a plate, or reword the note if code really draws it. The script refuses a regions file that leaves comp ink unnamed (callouts, a parts table, a notes block): what is never named can never be missing, so everything the comp shows gets a region. It also refuses a `text` / `control` / `chrome` region larger than a quarter of the comp: that is a column, not an element, and a column scored as one region hides the plates, tables, and notes inside it. Name each element inside it (`container: true` only when it truly is one undivided element). Anything drawn is a plate: an inline SVG past an icon's budget (a diagram, notation, leader lines with arrows, a "quick approximation" of the artwork) is refused at the hero; icon-sized SVG (under 64px, a few paths) is fine, and a chart the page draws from data at runtime is a chart, not an illustration. Callout lines and arrows that annotate a drawing belong to that drawing's plate, with only their labels set as text. A crop of the comp is never a plate (the plates gate refuses a file that is a resample of the comp region: the comp's grain, its neighbours' edges, and its resolution would ship as the artwork); the crop is the reference the plate is generated from. A plate region's box has to hold its whole artwork with a margin: the spec measures the artwork's contact with the box edges and refuses a box that cuts through it (`bleed: true` only when the page really crops it there), because a plate placed with `object-fit: cover` on such a box shows the artwork minus the side the box lost. Anything not in the spec does not exist on the page: no borders, rules, containers, or chrome the comp does not show. Only three concessions exist: fonts (the closest obtainable face), icon glyphs (close enough, exact if the user chose an icon library; this covers the pictogram only, never a control's chrome, so a chevron, an arrow, a dropdown's border and fill, a button's shape are the comp's), and genuine defects in the comp such as spelling errors. <!-- rule:skill-comp-spec -->
2. **plates.** Every raster region ships as a plate: an illustration, photo, or figure regenerated at asset resolution from its comp crop, UI text removed, at its `plate` path (ink on flat ground is generated on a chroma key and keyed to alpha, so it sits on the page's own ground rather than a second paper); a texture (paper, cloth, grain) is a clean patch of the comp region mirror-tiled to size, generated only when no clean patch exists. `generate-image.mjs --plate <id>` does one region end to end and scores it against the crop; a harness-native image tool takes the crop (`comp-spec.mjs --crop <id>`) as its input image and `comp-spec.mjs --plate-prompt <id>` as its prompt, then `embed-prompt.mjs`. With parallel subagents, spawn the shipped asset producer (`impeccable-asset-producer`; `impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent") with the spec path and let it produce them all; without subagents, produce them here. A crop of the comp is a reference, never a shipping pixel. The gate checks every plate exists, is at least 1.5x the region's size, and reads as the region. Page code waits for this gate: a page written before its plates exist is a page that draws its material in CSS. A single-file deliverable changes nothing here: the plate is produced the same way and inlined as a data URI. `--force` exists for one case only, the user downgrading the comp's authority in words you quote in `--reason`; the script refuses every other reason. <!-- rule:skill-plates-before-page -->
3. **hero.** `build-phase.mjs scaffold` first: it writes the measured layout as CSS custom properties (`.impeccable/build/scaffold/layout.css`: `--r-<id>-x/y/w/h` in % of the comp, plus cap height, font-size, family, and weight where measured) and a reference page (`hero-reference.html`) with every region at its box and every plate placed. Bind the numbers to your own semantic structure, an element per region; the reference is a check on positions, never the page, and overlapping boxes are overlapping boxes. Then build only the first viewport, at the comp's own dimensions, the comp's words copied verbatim (the user approved that comp with those words; rewording is a stated decision after the hero passes, never a silent one inside it), every text region sized from its measured cap height and set in its ranked face, plates first: place every plate at its spec box (`object-fit: cover`, an `<img>`, a background image, or an inlined data URI named for it) before any text or control, capture into `.impeccable/review/hero-repro.png`, run `build-phase.mjs record hero` once so you see the plate regions read as match before any text exists, then lay the semantic layer over the plates from the spec's palette and boxes and advance. The gate first refuses while any plate is unreferenced by the source, then runs `comp-diff.mjs`, writes `.impeccable/review/diff/hero/` (side-by-side, heatmap, one paired crop per region, `report.json`), and passes at 72% overall with no hard veto outstanding (a missing region, a contradicted plate or text block, an SVG illustration, a clipped plate, invented ink block at any score); above the bar, the numeric readings become advisories printed with the pass, and the polish pass before responsive is where they get fixed: the gate also reads each text region's cap height, line count, weight, ink colour, and position against the comp, each chrome strip's height off its rule, and the frame for ink where the comp is calm (a kicker, an extra nav item, a divider), and says each miss as a number ("cap height 78px in the build, 103px in the comp"); those numbers are the edit. When it fails, open the region crops it lists, in order, before editing: a region scored `missing` needs its material, `contradicted` needs its structure re-derived from the spec box, `drift` is where size and spacing edits belong; the gate refuses a third attempt that only nudges values on the same region. This is where the run's ambition is won or lost, and a retry here costs minutes where a rebuild verdict at the finish costs the run. <!-- rule:skill-hero-gate -->
1. **spec.** Measure the comp: `impeccable comp-spec --comp <comp> --grid` writes a coordinate grid over the comp; open it, name every salient region by grid span in a regions file (text and control regions snap to the largest ink mass inside their span, so a headline named B1:E4 measures as the headline and not the column beside it; `snap: false` keeps the span, and an explicit `box` is taken as drawn) (kind `plate` / `image` / `texture` for anything painted: every illustration, photograph, figure, product object, and material texture; `text` / `control` / `chrome` for what code draws; every region carries a `note` saying what the comp shows there, which the plate prompt and the gate messages read), and run `impeccable comp-spec --comp <comp> --regions <file>`. The spec carries each region's box, sampled palette, and medium; `impeccable comp-spec --print` is the build's reference from here on. Type is measured, not guessed: `impeccable font-match --measure <text region>` reads the comp's cap height, width class, and weight off the pixels, and `impeccable font-match --rank <region> --text "..."` takes its candidates from a fingerprint index of the Google Fonts catalog (the nearest faces to the crop's shape) plus any names you pass with `--candidates`, renders them at that cap height with the region's words, and ranks them by fingerprint distance (its `USE` line is the CSS; its proof sheet shows the comp over the top three); with no browser resolvable it records the catalog's nearest face and says the size is estimated, which is still the choice to build on. Do not install a browser to rank, and never write a `chosen` face into the spec by hand: the gate accepts only what font-match wrote. The spec gate refuses to close until the lead text region is measured and ranked. A region note that describes painted material (a diagram, drawing, photograph, texture) under a code kind is refused at the spec: reclassify it as a plate, or reword the note if code really draws it. The script refuses a regions file that leaves comp ink unnamed (callouts, a parts table, a notes block): what is never named can never be missing, so everything the comp shows gets a region. It also refuses a `text` / `control` / `chrome` region larger than a quarter of the comp: that is a column, not an element, and a column scored as one region hides the plates, tables, and notes inside it. Name each element inside it (`container: true` only when it truly is one undivided element). Anything drawn is a plate: an inline SVG past an icon's budget (a diagram, notation, leader lines with arrows, a "quick approximation" of the artwork) is refused at the hero; icon-sized SVG (under 64px, a few paths) is fine, and a chart the page draws from data at runtime is a chart, not an illustration. Callout lines and arrows that annotate a drawing belong to that drawing's plate, with only their labels set as text. A crop of the comp is never a plate (the plates gate refuses a file that is a resample of the comp region: the comp's grain, its neighbours' edges, and its resolution would ship as the artwork); the crop is the reference the plate is generated from. A plate region's box has to hold its whole artwork with a margin: the spec measures the artwork's contact with the box edges and refuses a box that cuts through it (`bleed: true` only when the page really crops it there), because a plate placed with `object-fit: cover` on such a box shows the artwork minus the side the box lost. Anything not in the spec does not exist on the page: no borders, rules, containers, or chrome the comp does not show. Only three concessions exist: fonts (the closest obtainable face), icon glyphs (close enough, exact if the user chose an icon library; this covers the pictogram only, never a control's chrome, so a chevron, an arrow, a dropdown's border and fill, a button's shape are the comp's), and genuine defects in the comp such as spelling errors. <!-- rule:skill-comp-spec -->
2. **plates.** Every raster region ships as a plate: an illustration, photo, or figure regenerated at asset resolution from its comp crop, UI text removed, at its `plate` path (ink on flat ground is generated on a chroma key and keyed to alpha, so it sits on the page's own ground rather than a second paper); a texture (paper, cloth, grain) is a clean patch of the comp region mirror-tiled to size, generated only when no clean patch exists. `impeccable generate-image --plate <id>` does one region end to end and scores it against the crop; a harness-native image tool takes the crop (`impeccable comp-spec --crop <id>`) as its input image and `impeccable comp-spec --plate-prompt <id>` as its prompt, then `impeccable embed-prompt`. With parallel subagents, spawn the shipped asset producer (`impeccable-asset-producer`; `impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent") with the spec path and let it produce them all; without subagents, produce them here. A crop of the comp is a reference, never a shipping pixel. The gate checks every plate exists, is at least 1.5x the region's size, and reads as the region. Page code waits for this gate: a page written before its plates exist is a page that draws its material in CSS. A single-file deliverable changes nothing here: the plate is produced the same way and inlined as a data URI. `--force` exists for one case only, the user downgrading the comp's authority in words you quote in `--reason`; the script refuses every other reason. <!-- rule:skill-plates-before-page -->
3. **hero.** `impeccable build-phase scaffold` first: it writes the measured layout as CSS custom properties (`.impeccable/build/scaffold/layout.css`: `--r-<id>-x/y/w/h` in % of the comp, plus cap height, font-size, family, and weight where measured) and a reference page (`hero-reference.html`) with every region at its box and every plate placed. Bind the numbers to your own semantic structure, an element per region; the reference is a check on positions, never the page, and overlapping boxes are overlapping boxes. Then build only the first viewport, at the comp's own dimensions, the comp's words copied verbatim (the user approved that comp with those words; rewording is a stated decision after the hero passes, never a silent one inside it), every text region sized from its measured cap height and set in its ranked face, plates first: place every plate at its spec box (`object-fit: cover`, an `<img>`, a background image, or an inlined data URI named for it) before any text or control, capture into `.impeccable/review/hero-repro.png`, run `impeccable build-phase record hero` once so you see the plate regions read as match before any text exists, then lay the semantic layer over the plates from the spec's palette and boxes and advance. The gate first refuses while any plate is unreferenced by the source, then runs `impeccable comp-diff`, writes `.impeccable/review/diff/hero/` (side-by-side, heatmap, one paired crop per region, `report.json`), and passes at 72% overall with no hard veto outstanding (a missing region, a contradicted plate or text block, an SVG illustration, a clipped plate, invented ink block at any score); above the bar, the numeric readings become advisories printed with the pass, and the polish pass before responsive is where they get fixed: the gate also reads each text region's cap height, line count, weight, ink colour, and position against the comp, each chrome strip's height off its rule, and the frame for ink where the comp is calm (a kicker, an extra nav item, a divider), and says each miss as a number ("cap height 78px in the build, 103px in the comp"); those numbers are the edit. When it fails, open the region crops it lists, in order, before editing: a region scored `missing` needs its material, `contradicted` needs its structure re-derived from the spec box, `drift` is where size and spacing edits belong; the gate refuses a third attempt that only nudges values on the same region. This is where the run's ambition is won or lost, and a retry here costs minutes where a rebuild verdict at the finish costs the run. <!-- rule:skill-hero-gate -->
4. **sections.** Build the rest of the surface inside the spec's system: the same corner language, line weights, and palette, and nothing the comp never shows. Where the comp does not cover a region, it inherits the recorded system.
5. **motion.** The signature interaction, reveals, and motion, orchestrated once rather than scattered.
6. **responsive.** The other viewports, and the first viewport at common desktop widths (1280 to 1600), not only at the comp's exact size: fluid columns, no fixed-pixel grid that wraps a hundred pixels narrower. Capture `desktop.png` (1440 wide, full page) and `mobile.png` (390 wide) into `.impeccable/review/`; the gate diffs the desktop capture against the comp and refuses a first viewport that only held at the comp's width. A comp'd surface that is mobile-first was comped portrait; the plates were produced for that frame.
@@ -132,17 +132,17 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi
## 7. Inspect and finish
Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. When the harness reports the user's actual viewport (an in-app browser's size, a named resolution), add that width to the set: the width that breaks is the one the user sees first. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. On a comp-led build, run `node {{scripts_path}}/comp-diff.mjs --comp <approved comp> --build .impeccable/review/desktop.png --spec .impeccable/build/spec.json --out-dir .impeccable/review/diff/final` and read its region rows and paired crops as the critique: the side-by-side is the view the build thread never has on its own, and a region it scores missing or contradicted is a fix whatever the page looks like from memory. Never judge fidelity from one full-page thumbnail; it hides exactly the failures that matter. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary.
Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. When the harness reports the user's actual viewport (an in-app browser's size, a named resolution), add that width to the set: the width that breaks is the one the user sees first. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. On a comp-led build, run `{{scripts_path}}/impeccable comp-diff --comp <approved comp> --build .impeccable/review/desktop.png --spec .impeccable/build/spec.json --out-dir .impeccable/review/diff/final` and read its region rows and paired crops as the critique: the side-by-side is the view the build thread never has on its own, and a region it scores missing or contradicted is a fix whatever the page looks like from memory. Never judge fidelity from one full-page thumbnail; it hides exactly the failures that matter. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary.
A capture is evidence only when it is valid, and you validate before you send. Settle or disable entrance motion first: an element hidden by animation timing reads as a missing element and gets fixed into a regression. Capture full-page shots from the document top. Capture the comp comparison at the comp's own pixel dimensions. Then open every file once and confirm it shows what its name claims: no black or blank regions, no wrong section behind a right filename, no half-loaded state. A malformed capture sent onward costs the whole round; the reviewer answers it with `disposition: recapture` and nothing it reviewed binds. <!-- rule:skill-capture-validity -->
After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node {{scripts_path}}/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`, plus `user-<width>.png` whenever the user's viewport joined the inspected set; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, every viewport you inspected is named required in the packet, and that directory is where it looks when a passed path is missing.
After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `{{scripts_path}}/impeccable detect --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`, plus `user-<width>.png` whenever the user's viewport joined the inspected set; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, every viewport you inspected is named required in the packet, and that directory is where it looks when a passed path is missing.
Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, the direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (a code-led build has no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), on a comp-led build the build state (`.impeccable/build/state.json`), the spec, and the diff directories (`.impeccable/review/diff/hero/` and `.impeccable/review/diff/final/`, whose side-by-side, heatmap, region pairs, and `report.json` are the fidelity evidence), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify the return carries the five contract sections (a recapture return carries one, its recapture list); on an empty or thrashed return, respawn once with the same inputs. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness with no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. <!-- rule:skill-finish-separate-reviewer -->
Act on the disposition word; there are exactly four. **recapture**: the evidence failed, not the build. Recapture what the return names under the capture-validity rules, then run a full review over the new evidence. A review conducted on invalid evidence binds nothing, and a verdict pass may never follow it. **rebuild**: fidelity failed wholesale, not in patches. Skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a fresh full review, never a verdict pass; a rebuild replaces regions wholesale, so the whole matrix runs again over the recaptures. Tell the user what is happening rather than asking permission to fix a failure. Consult the user only on a second rebuild directive, both verdicts on the table, or when rebuilding would discard content the user approved. **ship**: nothing is owed; report the verdict at its scope and continue to the documenter. **fix**: apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever decides, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Do not run a second detector. <!-- rule:skill-verdict-bounds-the-finish -->
A rebuild and a fix round share one asset rule: a raster either round creates or replaces is still asset work under [visualize.md](visualize.md)'s Produce section and keeps its **provenance** like every build raster, and a raster the round abandons is deleted in the same batch. Before either round's result goes back for review or verdict, run `node {{scripts_path}}/embed-prompt.mjs --scan <asset-dir...>` over the directories the artifact's rasters ship from and clear every file it reports by embedding what it is missing: the exact generation prompt for a produced raster, the origin for a sourced, stock, or pre-existing one. The scan only reads; deletion is reserved for rasters the round abandoned, never for a file the scan flagged. <!-- rule:skill-late-raster-provenance -->
A rebuild and a fix round share one asset rule: a raster either round creates or replaces is still asset work under [visualize.md](visualize.md)'s Produce section and keeps its **provenance** like every build raster, and a raster the round abandons is deleted in the same batch. Before either round's result goes back for review or verdict, run `{{scripts_path}}/impeccable embed-prompt --scan <asset-dir...>` over the directories the artifact's rasters ship from and clear every file it reports by embedding what it is missing: the exact generation prompt for a produced raster, the origin for a sourced, stock, or pre-existing one. The scan only reads; deletion is reserved for rasters the round abandoned, never for a file the scan flagged. <!-- rule:skill-late-raster-provenance -->
Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. <!-- rule:skill-user-evidence-reopens-review -->
+3 -3
View File
@@ -29,7 +29,7 @@ Use the feature yourself at the surface's representative sizes: desktop and mobi
If a prior critique exists, use it as one input:
```bash
node {{scripts_path}}/critique-storage.mjs latest "<resolved target>" --json
{{scripts_path}}/impeccable critique-storage latest "<resolved target>" --json
```
Exit 0 returns JSON with the latest snapshot's `body` and an exact `snapshot_file` identity. Retain `snapshot_file` until the end of the pass. For a local file target, the helper compares the file's exact current content fingerprint with the fingerprint captured by critique. Unchanged staged, unstaged, or untracked content remains current; any byte change, deletion, or replacement with a non-file closes the backlog it identified while preserving its trend history and exits 2. A URL target has no local fingerprint and remains current until explicitly closed. When current, incorporate relevant P0/P1 findings from `body` and name the snapshot read. Exit 2 means none exists or the target changed. Perform an independent pass either way.
@@ -92,14 +92,14 @@ Walk the complete path again with mouse, keyboard, and touch where applicable. C
- console errors, layout shift, interaction latency, and image loading everywhere; supported browsers on the web; supported OS versions, runtime warnings, and dropped frames on native;
- agreement with DESIGN.md, neighboring features, and the user's scope.
Follow the quality guidance supplied by `context.mjs` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment.
Follow the quality guidance supplied by `impeccable context` and hooks, then run any other relevant QA commands. Context requests a manual scan only when no automatic detector is active; never add another detector pass. Fix real defects and document only narrow intentional exceptions. A clean scan does not replace visual judgment.
Finish with a source diff: remove accidental churn, orphaned code, redundant values, and temporary artifacts. Ship only when the feature is functionally complete and consistently finished across the path.
When this pass clears every Priority Issue it took from a snapshot, close that snapshot:
```bash
node {{scripts_path}}/critique-storage.mjs close "<resolved target>" "<snapshot_file returned by latest>"
{{scripts_path}}/impeccable critique-storage close "<resolved target>" "<snapshot_file returned by latest>"
```
This closes only the snapshot this pass actually processed; if a newer critique landed meanwhile, its backlog stays live. Do not close when no snapshot was read, when `snapshot_file` was not retained, or when Priority Issues remain.
+3 -3
View File
@@ -2,7 +2,7 @@
Read this when the user invokes `{{command_prefix}}impeccable` with no argument. They are asking "what should I do?" Make the menu context-aware instead of static.
Setup has already run `context.mjs`. If that reported `NO_PRODUCT_MD`, the project has no captured context yet: lead the menu with `/impeccable init` as the top recommendation (one line on why) and still show the rest below; don't silently jump into init. Otherwise run `node {{scripts_path}}/context-signals.mjs` once and read its JSON, then lead with the **2-3 highest-value next commands**, each with a one-line reason pulled from the signals, followed by the full menu (the Commands table in SKILL.md, grouped by category). **Never auto-run a command; the recommendation is a suggestion the user confirms.**
Setup has already run `impeccable context`. If that reported `NO_PRODUCT_MD`, the project has no captured context yet: lead the menu with `/impeccable init` as the top recommendation (one line on why) and still show the rest below; don't silently jump into init. Otherwise run `{{scripts_path}}/impeccable signals` once and read its JSON, then lead with the **2-3 highest-value next commands**, each with a one-line reason pulled from the signals, followed by the full menu (the Commands table in SKILL.md, grouped by category). **Never auto-run a command; the recommendation is a suggestion the user confirms.**
Reason over the signals; there is no score to obey:
@@ -10,9 +10,9 @@ Reason over the signals; there is no score to obey:
- `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique <surface>` is a strong default.
- `critique.latest` with a low `score` or non-zero `p0` / `p1``polish` (it reads that snapshot as its backlog and closes it when stale or cleared).
- `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them.
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. **`live` and the bundled `detect.mjs` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with either; the browser overlay and the HTML rule engine don't apply to native app code.
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. **`live` and the bundled `impeccable detect` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with either; the browser overlay and the HTML rule engine don't apply to native app code.
- Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`.
**If `scan.targets` is non-empty and `setup.platform` is not `ios`/`android`/`adaptive`, run `node {{scripts_path}}/detect.mjs --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx; it reads HTML/CSS, so skip it for native projects). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
**If `scan.targets` is non-empty and `setup.platform` is not `ios`/`android`/`adaptive`, run `{{scripts_path}}/impeccable detect --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx; it reads HTML/CSS, so skip it for native projects). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede.
+1 -1
View File
@@ -24,7 +24,7 @@ When a sub-agent tool is available and permitted, run these independently; other
2. **Mechanical scan:** run:
```bash
node {{scripts_path}}/detect.mjs --json --scope type [target files or dirs]
{{scripts_path}}/impeccable detect --json --scope type [target files or dirs]
```
Also inspect dynamic or arbitrary font values the detector cannot interpret. Synthesize both assessments before editing, noting what each caught alone. A clean scan is a floor, not proof of good typography.
+8 -8
View File
@@ -1,14 +1,14 @@
# Visualize: Direction Comps & Asset Production
Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led contract skips this file by design, not by drift; do not load it then. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. A surface-scope structure round that already put three visualized cards before the user (new-work.md, established world) has discharged this round: the locked card's comp is the approved comp, so record the approval and continue at After approval; generate nothing new.
Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback `impeccable context` reports). A code-led contract skips this file by design, not by drift; do not load it then. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. A surface-scope structure round that already put three visualized cards before the user (new-work.md, established world) has discharged this round: the locked card's comp is the approved comp, so record the approval and continue at After approval; generate nothing new.
A probe tests composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed.
## Generate three compositional options
The comp round runs inside the build's phase state: `build-phase.mjs start --direction <seed key> --kind <...>` has already run (the roll's output names the command) and its `comps` phase is open before the first comp is generated; a comp rendered before that sits outside the state, and a session resumed from that point has no phases to follow. `generate-image.mjs` refuses to write under `.impeccable/mocks/` until start has run; a harness-native image tool is bound by the same order.
The comp round runs inside the build's phase state: `impeccable build-phase start --direction <seed key> --kind <...>` has already run (the roll's output names the command) and its `comps` phase is open before the first comp is generated; a comp rendered before that sits outside the state, and a session resumed from that point has no phases to follow. `impeccable generate-image` refuses to write under `.impeccable/mocks/` until start has run; a harness-native image tool is bound by the same order.
Render three distinct high-fidelity north-star comps of the requested surface, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything is built against it. Comps are the build thread's own work, never delegated: the thread that writes the prompts holds the direction's full context and has seen every comp when the build starts. Open every image by its workspace-relative path; sandboxed viewers reject absolute paths, and everything under the project root has a relative one. Base the comps on real content and the surface concepts already developed with the user. On an established world, anchor every comp on the real identity: capture a screenshot of a representative existing page and pass it as a reference image (the harness image tool's input image, or `generate-image.mjs --ref`); the prompt leads with the new surface's structure while the reference carries palette, type, and component character, because DESIGN.md words alone drift where a pixel reference does not. Name what the reference contributes and what it must not: chrome, palette, type, and component character carry over; the reference page's own content does not, and a banner, hero, or card lifted verbatim is the reference leaking, not fidelity. Three is the number: one comp invites rubber-stamping; the spread between three surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this discipline, so generate two more that vary what the first held fixed, and send all three to the approval point together. Only a round arriving with no decision comp (a degraded roll, an identity-mode page, a direction pinned without the decision round) renders all three here.
Render three distinct high-fidelity north-star comps of the requested surface, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything is built against it. Comps are the build thread's own work, never delegated: the thread that writes the prompts holds the direction's full context and has seen every comp when the build starts. Open every image by its workspace-relative path; sandboxed viewers reject absolute paths, and everything under the project root has a relative one. Base the comps on real content and the surface concepts already developed with the user. On an established world, anchor every comp on the real identity: capture a screenshot of a representative existing page and pass it as a reference image (the harness image tool's input image, or `impeccable generate-image --ref`); the prompt leads with the new surface's structure while the reference carries palette, type, and component character, because DESIGN.md words alone drift where a pixel reference does not. Name what the reference contributes and what it must not: chrome, palette, type, and component character carry over; the reference page's own content does not, and a banner, hero, or card lifted verbatim is the reference leaking, not fidelity. Three is the number: one comp invites rubber-stamping; the spread between three surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this discipline, so generate two more that vary what the first held fixed, and send all three to the approval point together. Only a round arriving with no decision comp (a degraded roll, an identity-mode page, a direction pinned without the decision round) renders all three here.
- A comp is a designed surface, not a picture of the subject. Lead the prompt with the surface's own structure: the regions this design has, named in order with their scale relationships; a page with no navigation says so instead of inventing one, and an unconventional surface states its unconventional skeleton. A prompt that leads with atmosphere gets a vignette back: the model paints the fish market instead of the fish market's website. Self-check every render: if it could hang as a poster, or reads as a photograph with some text on it, it is not a comp; regenerate with the layout scaffold stated more literally.
- The inverse is also a failure: a surface with none of its subject in it. The subject appears as the content the regions hold; the world dresses the frame and never displaces what the frame shows. The deletion usually rides in on the prompt's exclusion list, so exclusions bind invented claims, and a medium ban belongs to the committed imagery stance, never to caution. Before accepting a render, point at the subject; a render that depicts everything about the world and nothing of the subject fails however faithful its atmosphere. Regenerate with the subject's content named region by region.
@@ -23,24 +23,24 @@ Each comp is a direction test, not a screenshot specification. Core UI text, res
## One approval point
Show the three together on the decision page (`serve-question.mjs`, one option per comp with the comp as its hero), or in the harness only when it renders images inline; a text-only surface does not count as display. Ask what should carry forward, what feels false to the world, and whether the selected concept should be approved, combined, revised, or rejected. Then stop and wait. A structured simulated user counts as attended and receives the same question.
Show the three together on the decision page (`impeccable serve-question`, one option per comp with the comp as its hero), or in the harness only when it renders images inline; a text-only surface does not count as display. Ask what should carry forward, what feels false to the world, and whether the selected concept should be approved, combined, revised, or rejected. Then stop and wait. A structured simulated user counts as attended and receives the same question.
Do not begin code until the user approves a direction or explicitly delegates the choice. If they delegate, choose using the task brief, PRODUCT.md, and DESIGN.md, and state the evidence. Approval refines the task concept; it does not modify DESIGN.md.
This approval point has no substitute and no skip condition. When the structured question tool errors, fall back to the decision page; only after both fail may you treat the choice as delegated, and a delegated pick is recorded exactly as an approval is and disclosed in your first reply, not your last. The finish reviewer treats comp-round comps with no recorded approval as a material finding; decision comps under `.impeccable/mocks/decision/` are the direction round's hand, not comp-round output, and imply no approval on their own.
After approval, record the choice where tools can find it: the approved comp's path goes in the surface brief, and its `.json` prompt sidecar gains `"approved": true` (every comp generated through `generate-image.mjs` has one; create it if a native tool didn't). The sidecar travels with the mocks folder, so the approval survives sessions and machines that never see the brief, and it is what `build-phase.mjs advance` reads to close the comps phase. Summarize the composition and the parts of the comp that must not be literalized, return to new-work.md, record the direction contract from the approved concept, and build.
After approval, record the choice where tools can find it: the approved comp's path goes in the surface brief, and its `.json` prompt sidecar gains `"approved": true` (every comp generated through `impeccable generate-image` has one; create it if a native tool didn't). The sidecar travels with the mocks folder, so the approval survives sessions and machines that never see the brief, and it is what `impeccable build-phase advance` reads to close the comps phase. Summarize the composition and the parts of the comp that must not be literalized, return to new-work.md, record the direction contract from the approved concept, and build.
## After approval: the comp becomes a spec
The approved comp is a north star for translation into semantic, responsive, accessible code, never a license to recompose: keeping the palette and mood while redrawing the topology is a second art direction. Do not rasterize core UI text or controls. Do not substitute a different visual driver after approval without asking.
What the comp shows is measured, not remembered. new-work.md section 6 runs the build as phases (`build-phase.mjs`): the spec phase turns the comp into region boxes with sampled palettes (`comp-spec.mjs`), and the medium of every region follows from what the pixels are, never from what feels buildable: a figure, a product object, machinery, any illustration with perspective, shading, or drawing skill in it, and any texture by name (woven cloth, paper grain, fabric, leather, brushed metal) is a `plate` / `image` / `texture` region and ships as a raster; text, controls, chrome, diagrams with countable elements, flat shape systems, and anything that must move, scale, or respond are semantic. Writing "CSS" for a sculpted panel's finish, or a many-vertex `clip-path` for a torn edge, is the quiet deletion of the approved design; the detector's organic-clip-path and buried-raster rules and the hero gate's region scores catch it. Dropping an image-native region is a scope decision the user makes at the approval point, never a silent flattening after it. Generated imagery is a material, not a claim: evidence rules bind assertions, specs, testimonials, and photographs presented as real, never render fidelity.
What the comp shows is measured, not remembered. new-work.md section 6 runs the build as phases (`impeccable build-phase`): the spec phase turns the comp into region boxes with sampled palettes (`impeccable comp-spec`), and the medium of every region follows from what the pixels are, never from what feels buildable: a figure, a product object, machinery, any illustration with perspective, shading, or drawing skill in it, and any texture by name (woven cloth, paper grain, fabric, leather, brushed metal) is a `plate` / `image` / `texture` region and ships as a raster; text, controls, chrome, diagrams with countable elements, flat shape systems, and anything that must move, scale, or respond are semantic. Writing "CSS" for a sculpted panel's finish, or a many-vertex `clip-path` for a torn edge, is the quiet deletion of the approved design; the detector's organic-clip-path and buried-raster rules and the hero gate's region scores catch it. Dropping an image-native region is a scope decision the user makes at the approval point, never a silent flattening after it. Generated imagery is a material, not a claim: evidence rules bind assertions, specs, testimonials, and photographs presented as real, never render fidelity.
## Plates and provenance
Every raster region's plate is produced in the plates phase, before any page code, by the shipped asset producer or in the current thread (`generate-image.mjs --plate <id>`, or the harness image tool with the crop as input and the spec's plate prompt). Generation context is part of the asset: after generating any image with any tool, run `node {{scripts_path}}/embed-prompt.mjs <image> --prompt "<prompt>"` with the exact string the tool received (`generate-image.mjs` does this itself), so the intent lives inside the file; `--read` recovers it, `--scan <dir>` lists rasters still missing one. The embedded prompt plus the region's row in the spec is the raster's **provenance**, and every raster the artifact references carries it; a sourced, stock, or pre-existing raster embeds its origin instead. A raster created or replaced later, in a fix batch or a reviewer's rebuild, is produced the same way; a raster a fix abandons is deleted in the same batch.
Every raster region's plate is produced in the plates phase, before any page code, by the shipped asset producer or in the current thread (`impeccable generate-image --plate <id>`, or the harness image tool with the crop as input and the spec's plate prompt). Generation context is part of the asset: after generating any image with any tool, run `{{scripts_path}}/impeccable embed-prompt <image> --prompt "<prompt>"` with the exact string the tool received (`impeccable generate-image` does this itself), so the intent lives inside the file; `--read` recovers it, `--scan <dir>` lists rasters still missing one. The embedded prompt plus the region's row in the spec is the raster's **provenance**, and every raster the artifact references carries it; a sourced, stock, or pre-existing raster embeds its origin instead. A raster created or replaced later, in a fix batch or a reviewer's rebuild, is produced the same way; a raster a fix abandons is deleted in the same batch.
Convert images with a converter context.mjs reported at boot (the IMAGE_TOOLS line); probe only when it reported none, at most once per session, never per image.
Convert images with a converter `impeccable context` reported at boot (the IMAGE_TOOLS line); probe only when it reported none, at most once per session, never per image.
Return to [new-work.md](new-work.md) for the direction contract, the phased build, and the finishing pass.