Commit Graph
48 Commits
Author SHA1 Message Date
e2ff625b63 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>
2026-09-04 10:42:45 -07:00
Paul Bakaus f2d95cddc5 Fix critique ignore file tracking
AI-assisted change.
2026-07-29 14:28:14 -07:00
Paul BakausandClaude Fable 5 90f9eeb99b Split service layer into private impeccable-site repo
The public repo keeps the OSS promise surface: skill, CLI, extension,
tests, and the provider build. The site, labs, concept/composition
catalogs, image pipeline, Cloudflare functions, and authoring guide move
to pbakaus/impeccable-site.

concept-seed tests run against a synthetic fixture catalog; the plugin
icon and skill categories moved in-repo; build validation narrows to
README prose and non-site counts; release notes read from a sibling
impeccable-site checkout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 22:41:53 -07:00
Paul BakausandClaude Fable 5 7557935fdb Expand concept system: modes, ratings, re-roll, breadth strategy
Catalog: mode-aligned staging surfaces (persuade/operate/read/experience),
star ratings on approvals feeding challenger draw weights, family
retirements, authoring strategy and territory guide, rework and breadth
authoring rounds, composition mining from rejected worlds.

Seed: six challengers (two per tier), --reroll chains, --mode staging
filter, rating-weighted draws. New-work: Present/visualize/re-roll flow,
image-gen requirement, register-neutral vocabulary.

Pipeline: per-mode staging prompts with split frames, hero-from-board
reference generation, render-safety guards. Labs: ratings UI, unrated
filter, mode chips, composition approve-guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 22:10:10 -07:00
Paul Bakaus ead6ddabe5 Preserve experimental Live app-server workstream
Snapshot the current app-server implementation, shared Live optimizations, generated harness output, and in-progress site work before restoring polling as the primary runtime path.

Prepared with Codex assistance under maintainer direction.
2026-07-15 16:07:34 -07:00
Paul Bakaus 78b1b5878d Add rendered Live quality evidence
AI-assisted: Codex
2026-07-13 09:51:21 -07:00
Paul BakausandClaude Opus 4.8 e83e437cdd Release prep: skill v3.9.0, CLI v3.2.0
Bump skill 3.8.0 -> 3.9.0 (plugin.json, marketplace.json, plugin/ subtree,
regenerated provider harness output) and CLI 3.1.0 -> 3.2.0 (package.json).

Changelog (site/pages/changelog.astro):
- Skill v3.9.0: codex grid-background ban, /impeccable bolder design-system
  lock, critique sub-agent independence on non-Claude/Codex harnesses,
  bundled helpers under strict-permission harnesses, Codex hook manifest fix.
- CLI v3.2.0: codex-grid-background detector rule, external skills-symlink
  preservation on first install.

Also: gitignore nested hook.cache.json/hook.pending.json copies (anchored
patterns missed the generated harness dirs), and repoint CLAUDE.md/AGENTS.md
changelog docs at changelog.astro with concise, user-facing-only tone guidance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 01:50:04 -07:00
8cf2be110d feat(cli): interactive hook consent + unified .impeccable/config.json (#245)
* feat(cli): interactive hook consent + unified .impeccable/config.json

Make the design-hook install a conscious choice and unify scattered config
into one file.

Interactive consent
- On an interactive `skills install`/`update`, the CLI explains what the hook
  does and offers to install it (default yes), then records the per-developer
  decision in the gitignored `.impeccable/config.local.json`, so it never
  re-asks. A recorded decision or an already-installed hook short-circuits;
  `-y`/non-TTY keeps the historical install-by-default behavior; `--no-hooks`
  is a one-off skip that records nothing. The trigger keys on "is the hook
  installed?" + "is there a recorded decision?", not a brittle version check.

Unified config
- `.impeccable/config.json` (shared) and `.impeccable/config.local.json`
  (gitignored) now hold all Impeccable settings: hook settings under a `hook`
  key, plus top-level `updateCheck`. `/impeccable hooks` writes the `hook`
  subtree, preserving siblings. The hook runtime reads `hook.quiet` and
  `hook.auditLog`; context boot reads `updateCheck`. The legacy
  `IMPECCABLE_HOOK_DISABLED|QUIET|LOG` and `IMPECCABLE_NO_UPDATE_CHECK` env vars
  still work and override config; docs now lead with config and treat env vars
  as a legacy note.
- No backward compat for the pre-unification `hook.json`/`hook.local.json`
  (the hook shipped an hour ago; nothing in the wild uses it). This repo's own
  hook config is migrated to `.impeccable/config.json`.

The CLI and skill scripts are separate trees, so a small CLI-side config module
(cli/lib/impeccable-config.mjs) duplicates the config-path and .git/info/exclude
handling; comments flag the duplication.

Tests: new cli config unit test; skills-cli consent tests (declined skips,
accepted installs, --no-hooks records nothing); hook.test.mjs back-compat
removed and quiet/auditLog-from-config + gitexclude coverage added. Full suite
green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(hooks): preserve sibling config fields + resolve audit log from event cwd (Bugbot)

Two Bugbot findings:

- High: `/impeccable hooks` edits replaced the whole `hook` object with the
  merge-helper output, dropping fields those helpers don't manage — so an
  `ignore-value --local` could wipe the recorded install consent and make the
  CLI re-prompt. writeConfig now merges over the existing hook object, keeping
  consent/quiet/auditLog.
- Medium: config-based audit logging resolved hook.auditLog from process.cwd(),
  which can differ from the hook event's project root (and Cursor's pre-edit
  hook passed no cwd). The hook now stamps the resolved project root on the
  audit entry, and writeAuditLog reads config from entry.cwd when present.

Tests: a /impeccable hooks edit preserves consent + quiet; writeAuditLog
resolves config auditLog from entry.cwd, not the fallback cwd.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(hooks): resolve a relative auditLog path against the project root (Bugbot)

A relative hook.auditLog was read from the project root but written relative to
the hook process cwd, so when those differ the log went to the wrong place.
writeAuditLog now resolves a relative target (from env or config) against the
same project root it reads config from. Absolute and ~/ paths are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix hook consent recovery and smoke config

* Fix hook consent explainer for Cursor

* Fix empty hook target consent

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 02:42:19 -07:00
672517f76e Add automatic design hook install and exceptions (#170)
* docs: add PRD for design detector hook integration

Plans a PostToolUse hook for Claude Code and Codex that runs the
existing design detector after every relevant file write and feeds
findings back to the agent as advisory system-reminder context. No
implementation in this commit; covers UX, technical design, build
pipeline changes, distribution, coverage tradeoffs, and rollout.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: revise hook PRD with best-practices review

Folds in the P0/P1/P2 findings from an online best-practices critique
against the official Claude Code and Codex hook references plus 10+
2026 community guides and similar prior-art tools (claw-hooks,
claude-code-hooks-mastery).

Key changes:
- Exec form everywhere (Codex snippet was shell form), with Windows
  rationale.
- Default timeout dropped from 10s to 5s.
- Re-entrancy guard (CLAUDE_HOOK_DEPTH) and per-file edit counter.
- Session-scoped finding dedup promoted from open question to v1.
- Per-language inline-ignore syntax map (HTML/JSX/CSS/JS).
- Hard-skip rules for sensitive paths and generated/lock files.
- Honest framing about Claude Code lacking per-plugin hook disable.
- Honest framing about Bash-written files being invisible in v1.
- Codex Windows-not-supported call-out, feature flag note, trust ceremony detail.
- Optional NDJSON audit log via IMPECCABLE_HOOK_LOG.
- Findings cap lowered 8 → 5 with attention-budget rationale.
- Versioned envelope ([impeccable@1]) on rendered template.
- Expanded test plan, decision log, and stdin payload appendix.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(hooks): ship the design detector hook for Claude Code and Codex

Implements docs/hooks-prd.md: a PostToolUse hook that runs the
impeccable design detector after every Edit/Write/MultiEdit on a UI
file and pushes findings into the agent's next-turn context as a
short system reminder. Silent on clean files. Never blocks an edit.

Why this matters: today, design slop (side-tab borders, gradient
text, purple/cyan palettes, bounce easing, etc.) only gets caught
when a human notices or someone explicitly runs /impeccable audit.
The hook closes the loop at the moment slop is written.

What ships in v1
- skill/scripts/hook.mjs: PostToolUse entry. Reads stdin, runs the
  detector in-process (no `npx impeccable` cold start), emits
  hookSpecificOutput.additionalContext when fresh findings exist.
- skill/scripts/hook-lib.mjs: extracted helpers (config, cache,
  filter, render, audit log, runHook orchestrator). 100% unit-testable.
- skill/scripts/hook-session-start.mjs: SessionStart greeting,
  gated by a project-scannable probe + 30-day throttle.
- skill/scripts/hook-admin.mjs: backs /impeccable hooks
  on/off/status/ignore-rule/ignore-file/reset.

Hardening built in
- Re-entrancy guard (IMPECCABLE_HOOK_DEPTH) so the hook can never
  recursively spawn itself.
- Hard-skip regexes for sensitive paths (.env, .pem, id_rsa,
  secrets, credentials, .git) and generated/lock/build output. These
  fire before the file is even read; cannot be turned off via config.
- Path-traversal check on the inbound file_path.
- Session-scoped dedup keyed by (session, file, rule, line) so the
  same finding never lands in context twice. Prevents the ~12.5K
  wasted tokens per chatty session called out in the PRD.
- Per-(session, file) edit counter with a one-shot suppression
  notice on the 7th edit, silent after.
- Fail-open contract: every error path returns exit 0 with no
  stdout. Optional NDJSON audit log via IMPECCABLE_HOOK_LOG.

Three kill switches (precedence high to low):
1. IMPECCABLE_HOOK_DISABLED env var (1/true/yes/on, case-insensitive)
2. .impeccable/hook.json `enabled: false`
3. /impeccable hooks off slash command (writes the JSON)

Inline ignores are language-aware. `// impeccable: ignore <rule>` for
JS/TS, `<!-- impeccable: ignore <rule> -->` for HTML/Vue/Svelte/Astro,
`{/* impeccable: ignore <rule> */}` for JSX/TSX, `/* impeccable:
ignore <rule> */` for CSS. `*` matches any rule. Directive applies
to the next non-blank line. Same shape as ESLint, Stylelint, Biome.

Build pipeline
- scripts/lib/transformers/hooks.js: per-provider hooks.json
  builders, plus the slim .codex-plugin/plugin.json manifest.
- providers.js: emitHooks: 'claude' for claude-code, emitHooks:
  'codex' for codex and agents. Codex also emits emitCodexPlugin.
- factory.js: emits hooks/hooks.json next to the skills tree.
- build.js: syncs hooks/ into harness roots and into the slim
  plugin/ subtree; writes .codex-plugin/plugin.json. Build is
  idempotent (verified: 98 staged files unchanged across two runs).

Claude Code wiring uses exec form (command + args) and the
${CLAUDE_PLUGIN_ROOT} placeholder. Matcher: Edit|Write|MultiEdit.
`if:` glob filters to UI extensions before spawning Node. PostToolUse
timeout 5s, SessionStart timeout 3s.

Codex wiring uses ${PLUGIN_ROOT} (Codex's native placeholder),
matcher Edit|Write|apply_patch, no `if:` analog (the script does the
extension filter). macOS and Linux only; hooks are disabled on
Windows in current Codex builds. The trust ceremony and feature flag
are documented in README.md.

Routing
- /impeccable hooks lives outside the 23-command router table on
  purpose: it is plumbing, not a design skill. The hidden
  routing slot is added to SKILL.md alongside pin/unpin so the LLM
  knows to dispatch it. The 23-command count and all stale-count
  validators remain happy.

Tests
- tests/hook.test.mjs: 38 unit tests covering env parsing, config
  load + defaults + malformed, cache round-trip + GC,
  ignoreRules/minSeverity/inline ignores (all four languages),
  globbing with **/*/{a,b}, render template with cap + clamp + 0-line
  prefix drop, audit log NDJSON, payload event-name parameterization,
  re-entrancy, kill switches, sensitive-path + generated-path +
  traversal skips, allowlist filter, config ignoreFiles, edit
  counter cycle including the 7th-edit notice, MultiEdit and
  apply_patch payload shapes, detector throw swallow, malformed
  stdin, missing file race.
- tests/hook-build.test.mjs: 18 integration tests covering hook
  manifest shape (matcher, timeouts, exec form, if: glob, placeholders),
  Codex differences (${PLUGIN_ROOT}, no if:, no SessionStart),
  Codex plugin manifest (no inline hooks field to avoid the
  duplicate-file error), routing across the hooksJsonFor table, and
  presence of all three committed artifacts plus the bundled detector
  the runtime relative-import path depends on.

Full suite: 175 bun tests + 186 node tests, all green.

Docs
- README.md: new "Design hook" section explaining default behavior,
  per-project / global / inline disable paths, the JSON schema knobs,
  the audit log debug flag, and the slop / a11y coverage split.
- HARNESSES.md: flips the `hooks` row for Codex from No -> Yes
  (Claude was already Yes), adds a per-harness hook-surface table
  with the manifest location and matcher each provider uses.

Open questions from the PRD intentionally deferred to v2: Bash-write
blind spot, effort-aware suppression, Stop-hook session summary,
per-rule severity, async hook mode. None block v1.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix Codex hook scanning: apply_patch paths and co-located stylesheets

Parse file targets from Codex apply_patch command bodies, co-scan imported
and sibling CSS when UI components are edited, drop the git-sweep PostToolUse
group, and align Codex SessionStart manifest and trust docs with the official
hooks spec.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Gitignore hook session cache and drop local test HTML

Hook dedup/throttle state in .impeccable/hook.cache.json is per-project
runtime data like other .impeccable/ sidecars. Remove an untracked
bad-nested-flexbox scratch page from site/public/.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix Claude Code hook: drop Edit-only if filter so Write/MultiEdit fire

Claude's if permission rule binds to one tool name, so Edit(*.{…}) never
spawned the hook on Write or MultiEdit despite the matcher listing them.
Extension filtering now lives in hook-lib on both Claude and Codex.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Surface Cursor design findings via stop-hook followup

Replace dropped postToolUse additional_context with afterFileEdit recording
and a one-shot stop followup_message so anti-pattern nudges reach the agent.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix design hook packaging and scans

* Fix Cursor hook pending bucket fallback

* Fix Sass hook scan coverage

* Fix Cursor hook review findings

* Fix session start dead hook normalization

* Fix hook config and relative scan paths

* Remove SessionStart design hook

* Remove redundant afterFileEdit normalization

* Fix Cursor suppression and module style scans

* Fix sensitive path hook filter

* Fix disabled Cursor stop hook emission

* Refresh hook harness artifacts

* Fix Cursor hook manifest install

* Add hook ignore-value support

* Ignore hook runtime files locally

* Fix Codex plugin hook packaging

* fix: address PR review bot findings

Block numeric hook depth counters from re-entering.

Avoid following stylesheet imports from traversal-looking hook targets.

* fix: gate ignore-value suggestions by supported rules

Only render exact ignore-value commands when the same finding can be suppressed by ignoreValues.

* Package Codex plugin as hook-only

* Remove Codex plugin packaging

* Recover hook install probe plumbing

* Remove Codex hook packaging follow-up doc

* Remove extra hook docs and skill wording changes

* Install real design hooks via skills CLI

* Add provider hook smoke runner

* Fix Cursor hook delivery with preToolUse gate

* Simplify Cursor hook install to preToolUse

* Clarify confirmed hook exceptions

* Persist hook ignores in shared config

* Guard font hook exceptions

* Fix hook install after main rebase

* Fix hook scan target handling

* fix: address hook review findings

* Address hook review feedback

* Stabilize DeepSeek insert live fixture

* Fix Cursor hook Python shell write bypass

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-13 21:19:19 -07:00
Abdul WahabandGitHub 6163ca0529 Add Svelte-native live mode adapter (#179)
* Fix live preview state for framework components

* Complete stateful live preview coverage

* Record Svelte manual validation

* Fix Svelte live mode adapter

* Fix live Steer apply flow

* Fix Svelte live variant refresh recovery

* Fix live exit bar teardown

* Consolidate Svelte live DeepSeek sweep

* Reconcile Svelte live browser after main rebase

* Fix live accept review regressions

* Fix carbonize column-zero indentation

* Fix live poll lease expiry flake

* Fix Svelte shader preview capture
2026-06-02 00:08:57 -07:00
Abdul WahabandGitHub ab3a13245c Fix: ignore hook cache artifact (#189) 2026-06-01 12:32:53 -07:00
83dd99bf9f refactor(codex): drop the .codex/agents sidecar; rely on nested skill agents (#173)
Codex auto-discovers subagents bundled inside an installed skill's own
agents/ folder, so the separate .codex/agents/*.toml sidecar was redundant.

- cli: remove installCodexAgents/isCodexLikely and their install/update calls
- context.mjs: remove the CODEX_AGENT_MISSING self-heal directive
- build: drop codex agentFormat so no top-level .codex/agents is emitted; the
  nested in-skill .toml bundling is the whole delivery
- remove the tracked .codex/agents/*.toml and the gitignore exception
- docs + build.test.js updated for the nested layout
- CLI patch version bump; skill version unchanged

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 21:20:42 -07:00
e8e3665142 Live mode: staged AI copy edits (#158)
* feat(live): manual text-edit panel + Astro inject + stale-lockfile reap

Adds a manual text-edit popover under the live-mode bar so users can
retype copy directly without going through generate. The footer's
"Apply edits" button fires a manual_edits event; the server writes
the changes back to source via the new live-edit.mjs deterministic
file mutator. Mirrors the wrap+accept flow but skips variant generation.

New scripts:
- skill/scripts/live-edit.mjs: writes manual_edits back to source
- skill/scripts/live-text-rows.js: browser walker that surfaces every
  pure-text descendant of the picked element as an editable row

Touched scripts:
- skill/scripts/live-browser.js: text panel UI, CONFIGURING state hook
- skill/scripts/live-poll.mjs: manual_edits routing
- skill/scripts/live-server.mjs: manual_edits endpoint + handler
- skill/scripts/live-wrap.mjs: small adjustments to support the flow

Docs + tests:
- skill/reference/live.md: manual-edit section
- tests/live-edit.test.mjs, tests/live-text-rows.test.mjs

Also bundles two live-mode reliability fixes that surfaced during
manual testing of the feature:

1. live-inject now emits is:inline when the inject target is a .astro
   file. Astro otherwise processes the <script> tag and rewrites src
   to its own bundled URL, so the literal live.js never loads.

2. readLiveServerInfo now probes the lockfile PID with kill(pid, 0)
   and unlinks the stale lock if dead. Previously a crashed helper
   left server.json with a dead PID and live-poll reported "Live
   server not running" forever.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(live): inline contenteditable text editing

Replace the text-edit popover panel with inline contenteditable activation.
When an element is picked in CONFIGURING, every pure-text descendant becomes
contenteditable="true" directly on the page. Each blur-event fires a single-op
manual_edits save to source. Esc restores original text and stays in CONFIGURING;
successful save exits to PICKING. If Go is clicked while a save is in-flight,
the save completes before generate fires.

Deleted ~340 lines of panel UI (initTextPanel, openTextPanel, closeTextPanel,
renderTextRow, buildTextFooter, etc.). Added enableInlineEdit, disableInlineEdit,
onInlineBlur. Server contract unchanged; live-edit.mjs handles per-op saves as
before. Tests: 186 pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(live): hide annotation overlay during inline edit

Annotation overlay's click handler was intercepting clicks on contenteditable
text elements. Hide the overlay when inline-edit is enabled to allow text
selection and editing. Restore it when exiting inline-edit (if still in
CONFIGURING).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(live): edit content badge mode with batched saves

Replace automatic inline contenteditable on element pick with an explicit "Edit content" badge. The badge appears at the element's top-right corner when an element is picked. Clicking the badge enters a new EDITING state where:

- The contextual bar hides
- The annotation overlay hides
- The badge morphs to show Cancel + Apply buttons
- Text descendants become contenteditable inline

Edits are held in memory (input event tracking) until Apply is clicked, which fires a single batched manual_edits event with all ops. Cancel discards drafts without saving. This eliminates the annotation overlay interference that prevented clicking on text elements.

The EDITING state integrates with the main state machine and handles all exits (Esc, click-outside, teardown) cleanly.

All 186 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(live): use row.el.tagName for tag in applyEditing op

The applyEditing function was trying to use row.tag which doesn't exist on the row object. The tag should be the tagName of the text element itself (row.el.tagName.toLowerCase()).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(live): Edit content badge styling + auto-focus + separate buttons

- Edit content button now matches Go button styling (BP.accent background, BP.mark text, FONT, transitions, hover effects)
- Auto-focus first editable element when entering editing mode (50ms timeout)
- Separate Cancel and Apply buttons with 8px gap (no divider)
- Cancel uses muted styling (BP.hairline background, BP.textDim text)
- Apply keeps brand accent styling
- Remove all focus rings and outlines on edit badge buttons (no blue ring/outline in EDITING mode)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* feat(live): Subtle button UI + cursor positioning + better copy

- Change badge buttons to use impeccable-button aesthetic (ink background, surface text, hover to accent)
  - Removes aggressive styling conflict with Go button
  - No animations; simple 150ms background transition
  - Matches site design language (padding 0.625rem 1.5rem, 0.8125rem font, letter-spacing 0.03em)
- Shorter, clearer button copy: "Edit" instead of "Edit content", "Save" instead of "Apply"
- Fix cursor positioning: cursor now appears at END of text, not beginning
  - Use Selection API to collapse cursor to end of contenteditable element
  - Improves UX for immediate continuation of text
- Update live.md documentation to reflect new button labels

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* fix(live): Use site design system colors for edit badge buttons

- Edit/Save buttons: oklch(10% 0 0) background → oklch(60% 0.25 350) on hover
- Cancel button: oklch(55% 0 0) background → oklch(65% 0 0) on hover
- All buttons: 6px border-radius (matches Go button), oklch(98% 0 0) text
- Smooth transition: 0.3s cubic-bezier(0.16, 1, 0.3, 1) (--ease-out)
- Uses site color palette instead of live-overlay constants

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* fix(live): Match slop-callout style for edit badge buttons

- Use exact .slop-callout aesthetic: paper background, accent border + text, uppercase 10px (0.625rem)
- 600 weight, 0.06em letter-spacing, 4px 8px padding, 6px border-radius
- Box-shadow: 0 2px 8px rgba(0,0,0,0.1) matches site callouts
- Hover: inverts to filled background (accent fill, paper text)
- Cancel uses ash color variant for muted state, Save uses accent
- Smooth 0.3s cubic-bezier(0.16, 1, 0.3, 1) transition on background and color

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(live): Pill-shaped edit badge buttons, 2px padding, no uppercase

- Border-radius: 999px (pill shape)
- Padding: 2px 8px (more compact)
- Removed text-transform: uppercase

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(live): Cancel button uses mist border + ash text

- Border: 1px solid oklch(92% 0 0) (--color-mist)
- Color: oklch(55% 0 0) (--color-ash)
- Hover: inverts to ash background with paper text

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(live): Remove blue focus outline from contenteditable elements in EDITING mode

- Add inline outline: none on each row's element when contenteditable activates
- Inject [data-impeccable-editable] CSS rule to override browser default focus ring
- Use !important to win against site styles that re-apply focus outlines
- Cleanup restores outline/data-attribute on disable

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(live): Decouple manual edits from agent/poll pipeline

Manual text edits now POST directly to a new /manual-edit endpoint
that runs live-edit.mjs synchronously and returns the result. The
event is never enqueued, never reaches the poll loop, never reaches
the agent.

Why: every Save was costing an LLM turn. The poll script would
dequeue the manual_edits event, run live-edit.mjs deterministically,
post a completion ack, then print the event JSON to stdout. The
Claude agent would read that output and decide "loop and re-poll".
Zero real work for the agent but every Save burned context.

Changes:
- live-server.mjs: new POST /manual-edit handler that runs live-edit.mjs
  synchronously and returns the result. Does not enqueue, does not log
  to session store. Defense-in-depth: /events rejects manual_edits.
- live-browser.js: applyEditing() POSTs to /manual-edit instead of
  sendEvent({type: 'manual_edits'}).
- live-poll.mjs: removed manual_edits handler branch (dead code now).
- reference/live.md: removed "Handle manual_edits" section; replaced
  with a one-line note that manual edits are server-direct.

The HMR-triggered page reload remains (dev server detects source file
change) but that is a separate dev-server behavior, not our pipeline.
resumeSession() already restores variants and selection after reload.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(live): Stash manual edits server-side; commit via AI on request

Decouples manual-edit Save from source file writes. Save now stashes
to .impeccable/live/pending-manual-edits.json with no HMR refresh.
The user explicitly asks the AI to commit when ready.

Why: even with the prior /manual-edit fix, every Save still wrote to
source and triggered the dev server's HMR/full reload. The page flash
was the actual user pain. Now there's zero source touch on Save, and
the user controls when the dev server reloads.

Server (live-server.mjs):
- /manual-edit-stash POST: append to buffer file. Returns {ok, pendingCount, totalCount, perPage}.
- /manual-edit-stash GET: query counts by page for counter UI.
- /manual-edit-discard POST: drop entries (all if no pageUrl).
- Old /manual-edit returns 410 Gone (defense in depth).
- Buffer ops merge by (pageUrl, ref): keep first originalText, update newText.

CLIs:
- live-commit-manual-edits.mjs: read buffer, shell out to live-edit.mjs
  per entry, truncate succeeded entries, surface failures.
- live-discard-manual-edits.mjs: truncate buffer (optionally scoped by page).
- Both take optional --page-url=<url>.

Browser (live-browser.js):
- applyEditing() POSTs to /manual-edit-stash, no source write.
- Pending pill (• N staged) + trash icon next to Exit in global bar.
- One-time onboarding toast on first Save: "Saved. Tell the AI to commit when ready."
- Counter persists across reloads via GET /manual-edit-stash on init.
- Trash icon: confirm dialog scoped to current page, then POST /manual-edit-discard.

Variant pipeline interaction:
- live-wrap.mjs: when wrapping an element, apply pending manual edits to
  the source range so the wrap block's "original" variant reflects the
  user's edited DOM (their pre-Go view), not the raw source.
- live-accept.mjs: after accept writes the variant to source, scrub
  buffer ops whose originalText no longer appears in that file. The
  accept embodies the manual edit; the pending op is consumed.
- Variant discard does NOT touch the buffer.

Reference docs:
- reference/live.md: full commit/discard contract, trigger guidance
  (narrow action-verb intent), do-not-auto-commit rule.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(live): Staged-edits pill becomes an "Apply" button

Click the "• N staged" pill → confirm dialog "Apply N staged edits
to source? The page will reload." → POST /manual-edit-commit on the
server, which shells out to live-commit-manual-edits.mjs. Same path
the AI uses, just triggered from the overlay.

Trash icon stays for discard. The AI-driven commit path also stays
(useful for inspecting failures or scripting). The pill is now the
primary apply affordance because it removes the chat-context-switch
for the common case.

Pill styling: pointer cursor, accent border + text at rest, fills
on hover (accent bg, paper text). Tooltip: "Click to apply staged
edits to source".

First-save toast updated: "Saved. Click the 'staged' badge to apply,
or ask the AI."

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore(live): gitignore pending-manual-edits.json runtime buffer

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: drop stray site/ test edits from PR

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(live): Pill label reads "Apply N staged"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(live): Manual edit ops use the leaf element's locator, not parent's

Multi-row inline editing captures each contenteditable leaf (row.el) but
the op was being built with selectedElement.id / classList — i.e. the
parent card, not the editable text node. live-edit.mjs then searched
source for the parent's class on the leaf's tag (e.g. <span class=
"foundation-card">), found nothing, and silently failed.

Use row.el's own id / classList instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(live): Climb to nearest classed ancestor when leaf has no locator

A bare <em>/<strong>/etc. with no id or class produced ops the CLI
rejected with insufficient_locator. Prefer the leaf's own id/class; if
neither exists, walk up to the nearest ancestor with one and adopt its
tag + locator. Text-replace still works because the CLI narrows by
originalText inside the matched element's source range.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(live): Make mixed-content paragraphs editable

The text-rows walker skips elements with mixed children (text + element +
text), so paragraphs like "Some text <code>x</code> more text" or "Body
text · <a>link</a>" exposed zero rows for the surrounding copy. At edit
time, wrap each non-whitespace direct text-node child in a marker span so
the walker emits a row for it. Unwrap on save/cancel. The locator climbs
to the parent's class as before, and live-edit narrows by originalText
inside that parent's source range.

hasTextRows now uses a lightweight subtree check that matches the new
wrap+walk path so the edit affordance shows up on mixed-content elements.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(live): Address Cursor Bugbot findings (CB-2 through CB-6)

CB-2 - Escape reverted DOM text but inlineEditDrafts retained the
pre-revert value; clicking Apply afterwards committed the undone edit.
Clear the draft entry when restoring innerText.

CB-3 - The scrub gate !result.handled || result.handled !== false was
a tautology that ran the scrub regardless of accept outcome. Use the
intended result.handled !== false.

CB-4 - The buffer-aware "original" content step in live-wrap iterated
every entry in the buffer with no pageUrl filter, so an edit on /a
could leak into a wrap call on /b. Add --page-url to the CLI; filter by
it; skip the buffer-aware step entirely when omitted. live.md updated.

CB-5 - removeEntries returned entry count while truncateBuffer returned
op count, causing the discard CLI and HTTP endpoint to report mixed
units. Make removeEntries return ops removed.

CB-6 - applyTextReplace used string truthiness to gate prepending
content above the edit, which silently dropped a leading empty line
when the file started with '\n'. Gate on the line index instead, and
mirror the fix on the trailing-empty-line side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(live): A3+A4 data-integrity guards, A6 test coverage

A3 — applyTextReplace refuses with text_ambiguous_in_block when
originalText appears more than once in the matched element block.
Refusing is safer than picking the first indexOf hit when we can't
tell which leaf the user edited; user can rephrase one occurrence.

A4 — newText is rejected if it contains <, >, {, }, or a backtick.
Two layers: server-side validator in /manual-edit-stash returns 400,
CLI-side guard in applyTextReplace returns invalid_chars_in_newText.
Browser surfaces the specific reason via toast. The shared char list
lives in live-edit.mjs (validateNewTextChars). reference/live.md
documents the rule.

A6 — New test files cover the orchestration gap:
 - live-manual-edits-buffer.test.mjs (17 tests across read/stage/
   remove/find/count/truncate; pins removeEntries returns OPS count)
 - live-wrap-buffer-aware.test.mjs (3 tests; CB-4 regression test)
 - live-commit-manual-edits.test.mjs (4 tests; partial-failure,
   --page-url scope, no_pending_edits)
 - live-discard-manual-edits.test.mjs (3 tests; CB-5 unit consistency)
 - live-accept-scrub.test.mjs (4 tests; keep/drop/prune)
Plus 2 new cases in live-edit.test.mjs for A3 and A4.

Side-effect refactors:
 - scrubManualEditsAgainstFile accepts cwd for unit-testing and is
   exported.
 - Failed-op entries in live-edit.mjs now propagate forbidden and
   occurrences fields so callers can surface specifics.

41 tests across the 6 affected files pass; full suite green at 186/186.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: drop .claude/pr-review.md from PR

Local review notes belong in the working tree, not the PR diff. Kept
in the file system; just untracked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: drop stray site/ test edits from PR (round 2)

Live-inject script tag and the "Impeccable Works!" / "WHAT'S INCLUDED
IN THE BOX" / "Wow Impeccable. ---- " strings were test edits that
slipped back into the branch. Restore both files to match main.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(live): Disable Edit badge while variants are generating

Clicking Edit during GENERATING would open inline text editing on the
same DOM region the variant wrapper is about to land in, racing the
HMR and the mutation observer. The badge now switches to an
'idle-disabled' rendering (ash + mist, not-allowed cursor, disabled
attribute, tooltip) the moment state transitions into GENERATING.
Returns to 'idle' on the normal CONFIGURING re-entry paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(live): live-wrap refuses without --page-url when buffer has pending edits

When a manual edit is staged ("Impeccable Works!") but not yet committed,
the buffer holds the user's edited DOM while source still has the un-
edited text ("Impeccable"). live-wrap's buffer-aware step exists to
rewrite the wrap block's <div data-impeccable-variant="original"> to
match the staged DOM, but per CB-4 it is gated by --page-url. When the
agent invoking live-wrap omits --page-url, the buffer-aware step
silently no-op'd and the variant authoring saw stale source — the
user's manual edit appeared lost.

Make the silent no-op a loud error: when buffer.entries.length > 0
and --page-url is missing, exit 1 with
{ error: 'missing_page_url_with_pending_edits', pendingEntries, hint }.
Empty buffer = no risk = no requirement, so existing flows without
pending edits keep working.

Updated reference/live.md to flag --page-url as required when the
buffer has entries. Added regression test in
live-wrap-buffer-aware.test.mjs. live-wrap.test.mjs gained a buffer-
clear hook so any leftover .impeccable/live/pending-manual-edits.json
from local dev doesn't trip the new check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* change back

* chore: drop stray site/ test edits from PR (round 3)

Live-inject script tag in Base.astro slipped back in via git add -A
while a local live server was running. Restore both site/ files to
main.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix live manual edit staging

* Rename live edit copy badge

* Use sentence case for live edit copy badge

* Move copy edit apply control outside live bar

* Improve live copy edit apply flow

* Clean up live copy edit AI apply flow

* Polish live copy edit docs and toast

* Fix staged copy edit review issues

* Fix CI jsdom dependency

* Fix Cursor Bot live edit findings

* Fix remaining live edit review issues

* Fix Bugbot staged edit edge cases

* Fix latest Bugbot live edit edges

* Fix remaining Bugbot wrap and discard issues

* Fix live copy edit safety contracts

* Fix copy edit rollback coverage

* Fix live manual copy edit apply flow

* Adjust live pending dock offset

* feat(live): route manual-edit Apply through the chat agent

Make the staged copy-edit Apply work when no CLI AI runner is
authenticated by routing the batch through the active chat session,
and surface runner failures clearly instead of opaque exit codes.

- live-poll: add --reply --data '<json>' so the chat agent can return
  a structured manual_edit_apply result (the documented flag was
  missing, so the server resolved with an empty object)
- live-server: manual_edit_apply event + deferred map, chat-vs-subprocess
  dispatch in /manual-edit-commit, resolve the deferred from the ack
- live-copy-edit-agent: chat provider, extractRunnerErrorMessage and
  commandAuthed pre-flight, diagnostic describeNoProviderError; drop the
  stale CLAUDE_CODE_SIMPLE and --no-session-persistence flags so headless
  CLAUDE_CODE_OAUTH_TOKEN auth works
- live-browser: clear pendingApplyInFlight on commit_done and add a
  watchdog so a missed signal can no longer freeze element picking
- reference/live.md: tight Handle manual_edit_apply handler plus a
  separate diagnostics reference section; advertise the event in the
  opening contract and dispatch table

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add live manual edit apply coverage

* Fix manual edit apply review issues

* Fix manual edit review follow-ups

* Fix manual apply poll acknowledgements

* Fix manual apply failed-entry rollback

* Clarify manual apply LLM prompt

* Fix stale manual apply discard events

* Fix manual apply dynamic source edits

* Fix large manual apply chunks

* Clarify manual edit apply is first-class work

* Clarify manual apply resume flow

* Compact live manual apply evidence

* Reject malformed manual apply replies

* Recover legacy manual apply summaries

* Fix Astro live script injection

* Add live manual edit apply coverage

* Slim live manual apply flow

* Slim manual edit test dependencies

* Stabilize real browser LLM smoke

* Generalize manual edit LLM prompt examples

* Remove retired live edit wrapper

* Inline live text row walker

* Slim manual edit prompts

* Drop AGENTS doc churn

* Stabilize live manual apply prompts

* Stabilize manual apply visible Haiku flow

* Add hard framework manual edit coverage

* Stabilize manual edit LLM retries

* Fix manual apply transaction rollback

* Fix live shader text capture

* Clean up manual apply runtime artifacts

* Fix live manual edit apply reliability

* Clean up manual apply coverage

* Slim manual apply test cleanup

* Fix manual edit prompt contract test

* Align manual edit cancel hover

* Fix live loading shader capture

* Fix manual apply review findings

* Restore live e2e tests for CI

* Fix live loading shader halftone

* Tune live loading shader dots

* Restore main live shader behavior

* Fix manual apply review findings

* Fix manual apply bot follow-ups

* Clarify manual apply rollback changes

* Fix manual apply state naming

* Address PR review cleanup

* Fix manual apply review follow-ups

* Fix multiline manual apply verification

* Restore inline drafts when hiding live bar

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 19:02:12 -07:00
Paul BakausandClaude Opus 4.8 92b744beb0 feat(site): new Neo Kinpaku social card, sitewide OG default
Replaces the retired light/magenta OG card with a brand-true Kinpaku
card (lacquer ground, champagne Alumni Sans headline, kinpaku-gold
accent, kintsugi-seam art). Headline: "Design fluency for every AI
harness." Command count is read live from command-metadata.json.

- scripts/generate-og-image.js: rewritten to render the Kinpaku card
  via Playwright at 2x and downscale with sharp; outputs og-image-v2.jpg
- Base.astro: emit og:image + summary_large_image on every page with a
  sitewide default (was homepage-only); pages override via ogImage prop
- og-image.jpg renamed to og-image-v2.jpg for cache-busting; index.astro
  reference updated
- CLAUDE.md: document `bun run og-image` regeneration + cache-bust steps
- .gitignore: ignore .og-build scratch

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 17:19:27 -07:00
9ffd3211d5 Neo Kinpaku design system + Live Mode v3 (#169)
* Add neo kinpaku design system page

* skill: rip out baked-in category recipes and saturated-default motion tropes

Programmatic bias mining (impeccable-evals) traced four major defects
back to specific lines in this skill that contradicted SKILL.md's own
first-order-reflex warning:

- brand.md "Pairing and voice" prescribed four category→aesthetic
  recipes (editorial → serif+sans, tech/dev/fintech → tight tracking,
  consumer/food/travel → script/display serif, creative → rule-break).
  These directly drove OpenAI's 76% extreme-negative letter-spacing
  on tech briefs and Anthropic/Google's 28-34% italic-serif-display
  slop on editorial/food briefs. Replaced with one sentence: the
  shape depends on the brand, not on the brand's category.
- brand.md "Brand permissions" had "Typographic risk. Enormous
  display type, unexpected italic cuts, mixed cases, hand-drawn
  headlines, a single oversize word as a hero." — a four-for-one
  slop driver behind 97% OpenAI comically-large H1, 42% bad-SVG
  illustration, and the editorial-italic slop. Deleted outright.
- typeset.md and teach.md repeated the same category recipes;
  trimmed to the principle without the recipe.
- SKILL.md Typography: added a hard hero-H1 ceiling (clamp() max
  ≤ 6rem ≈ 96px), with a <codex> block to make it explicit since
  OpenAI over-indexes here (97% ≥128px vs 24% for Anthropic).
- animate.md, bolder.md, brand.md: removed "staggered reveals" and
  "scroll-triggered transitions" as the prescribed default ambitious
  motion. By 2026 that's the saturated AI tell, not a choreography.
  Reserved stagger for legitimate list-sibling rhythm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* skill: anti-cream + codex-specific defect bans + universal slop bans

Second pass after measuring more biases against the eval corpus.

- SKILL.md Color: explicit "cream/sand/beige body bg is the saturated
  AI default of 2026" rule. Tone down the "tint every neutral" line so
  it doesn't read as "default to warm-tinted near-white" (which OpenAI
  hits at 74% and Anthropic at 31%-47%).
- SKILL.md Absolute bans: add universal bans for two slop patterns
  detected at 55-95% across providers — tiny uppercase tracked eyebrow
  above every section (the 2023-era kicker that's now AI grammar) and
  numbered section markers (01/02/03). Also explicit "text that
  overflows its container is the universal defect on tablet/mobile."
- SKILL.md Absolute bans → <codex> block: ban the GPT-specific defects
  Paul annotated repeatedly — `border:1px solid` + soft-wide-shadow
  (≥16px blur) "ghost cards", `border-radius:32px+` over-rounding,
  hand-drawn/sketchy SVG illustrations (loose-sketch / *-sketch classes,
  feTurbulence paper-grain filters), repeating-linear-gradient stripes,
  "X theater" AI-slop copy phrases.
- SKILL.md Motion → <gemini> block: the image :hover transform tell
  (38% Google skill-on rate). Hover effects on images add no info; the
  image isn't an action target. Animate card chrome, not the image.
- SKILL.md Typography: hard display letter-spacing floor ≥-0.04em
  (OpenAI defaults to -0.075em → cramped). Existing hero ceiling
  <codex> block extended with the letter-spacing rule.
- codex.md Step A example: stop seeding "warm-grounded (deep oxblood +
  cream)" as the warm-palette template, which primes the cream default.
- colorize.md Tinted backgrounds: stop printing the literal cream
  recipe `oklch(97% 0.01 60)`; replace with brand-anchored guidance.
- document.md examples: warm-ash-cream → cool-paper so the example
  doesn't seed cream as the canonical neutral example.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* skill: universal anti-slop bans + contrast/font-count/all-caps-body rules

Third pass after measuring the rest of the cross-provider matrix:

- Color: explicit "Verify contrast" rule. Low-contrast text fires at
  68% across all providers skill-on (90+% off). The most common
  failure is muted gray body on a tinted near-white; light-gray-for-
  elegance is named as the single biggest cause of unreadable AI
  pages.
- Typography: max-3-font-families rule. Overused-fonts (>4 families)
  fires at 28% Anthropic / 36% Google / 0% OpenAI skill-on; >50% off.
  Also: universal "no all-caps body copy" (moved from brand-only ban
  to Shared design laws since product-register also overuses caps).
- Copy: anti-aphoristic-cadence ban targets Anthropic's signature
  "X. No Y." / "X. Just Y." voice (63% skill-on copy-slop rate, 77%
  off — the worst rate in the matrix). Once-is-voice / three-or-more-
  is-tell framing per the runner's copy-slop detector.
- Copy: anti-SaaS-buzzword-string ban with the literal phrase list
  the detector watches for (streamline/empower/supercharge, trusted-
  by-leading, best-in-class/enterprise-grade/cutting-edge, etc).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* skill: strengthen anti-cream rule across full warm-neutral band

Smoke validation showed the cream fix worked for Google + OpenAI but
Anthropic Sonnet italian-restaurant still shipped `--paper: oklch(90%
.018 88)` — cream just outside the L≥95% band the rule cited.

Broaden the rule:
- Band: OKLCH L 0.84-0.97, C < 0.06, hue 40-100 (was 95-97% / 60-95).
- Name the token-name tells explicitly (paper / cream / sand / bone /
  flour / linen / parchment / wheat / biscuit / ivory) — the model
  defaults to one of these regardless of what hex it lands on.
- Call out the specific brief patterns ("warm, traditional, family-
  coastal-Italian" / "editorial-restraint") that the model translates
  into cream by reflex. Then provide three explicit non-cream options:
  saturated brand color, true off-white at C=0, or darker mid-tone.

Warmth in the brand is carried by accent + typography + imagery, not
by body bg.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* v3.2.0: skill bias-fix release

Bumps version from 3.1.1 to mark the four-commit skill cleanup that
rips out baked-in category recipes (brand.md), saturated-default motion
tropes (staggered reveals everywhere), the cream/sand body-bg AI tell,
codex-specific defects (1px+wide-shadow, over-rounding, hand-drawn SVGs,
stripes, X-theater copy), the extreme-letter-spacing default, and
universal slop bans (all-caps eyebrow on every section, numbered-section
markers, all-caps body, font-family-count > 3, aphoristic copy cadence,
SaaS buzzword strings). Plus a hard hero-H1 ceiling (clamp() ≤6rem) and
a Gemini-specific image:hover transform block.

Validated against ~190 post-fix samples — see impeccable-evals
biases tab for per-provider deltas.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* drop "no pure black/white" rule entirely

The rule was contested in the design world and causing more damage than
good — pushing every page into the tinted-near-white default which is
the cream/sand AI tell we already explicitly ban elsewhere. Vercel,
SVKMS, Brutalist sites, et al. use pure black/white successfully; the
skill shouldn't second-guess that.

Skill markdown deletions:
- SKILL.md Color: drop the "Never use #000 or #fff" bullet.
- color-and-contrast.md: drop the "Never Use Pure Gray or Pure Black"
  subsection, the "Never pure black" table-row prescription, and the
  "Avoid: Using pure black for large areas" bullet.
- colorize.md: drop the "NEVER use pure black or pure white for large
  areas" bullet.
- polish.md: drop the "Tinted neutrals: No pure gray or pure black"
  half of the bullet (the gray-on-color bullet survives).

Detector code (cli/engine):
- registry/antipatterns.mjs: remove the `pure-black-white` entry.
- rules/checks.mjs: remove the three `findings.push({ id:
  'pure-black-white', ... })` emit points (inline #000 bg, Tailwind
  bg-black class, plain-HTML scan path).
- engines/regex/detect-text.mjs: remove the two pure-black-white regex
  rules (CSS `background: #000…` + Tailwind `bg-black`).
- detect-antipatterns-browser.js: regenerated via
  scripts/build-browser-detector.js.

Tests:
- detect-antipatterns-fixtures.test.mjs: invert the assertion that
  pure-black-white fires; expect it to NOT fire post-v3.2. Drop the
  Tailwind bg-black-opacity edge-case test (no longer relevant).
- detect-antipatterns.test.js: drop the standalone "detects pure-
  black-white in styled-components" test and remove pure-black-white
  from the multi-detector assertions in PricingCard, globals.css, and
  GlobalStyle.tsx tests.

166 bun tests pass; 24 node fixture tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* skill: strip example patterns from copy rules, strengthen gemini block

v3.2 rerun validation surfaced two issues:

1. Copy-slop detector fires more on Gemini under v3.2 (48% → 84%) than
   under no-skill baseline. Root cause: the anti-aphoristic-cadence rule
   printed the literal "X. No Y." / "X. Just Y." patterns as examples,
   and Gemini imitated them as the recommended voice. Same recipe-becomes-
   bias trap we hit with brand.md:116's "Enormous display type, unexpected
   italic cuts, mixed cases, hand-drawn headlines" enumeration. Fix:
   describe the cadence as a rhythm ("serious statement, then punchy
   short negation") without printing literal patterns. Buzzword list
   trimmed to a single inline phrase family rather than quoted strings.

2. Gemini image:hover transform Gemini-tell hadn't dropped (31% off →
   32% v3.2). Strengthen the <gemini> block: explicit "Never animate
   <img> elements on hover", call out the Tailwind group-hover:scale /
   group-hover:rotate / group-hover:translate parent-hover patterns by
   name (Gemini was reaching for these via Tailwind even though the
   prior text talked about :hover on the image directly).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* skill: simplify context loading and inline register directive

Replaces load-context.mjs's JSON output with a tight markdown block from
the renamed context.mjs. The script now extracts PRODUCT.md's `## Register`
field and appends a `NEXT STEP:` directive naming the matching reference
(brand.md / product.md), which moved Gemini from skipping the register
load entirely to honoring it. Drops the `.impeccable.md` auto-migration;
makes IMPECCABLE_CONTEXT_DIR a lazy escape hatch consulted only when the
default paths come up empty.

Setup is now four bullets in one list. The DESIGN.md nudge is gone; in
its place, a "familiarize with the existing design system" step that
calls out CSS / tokens / running app as authoritative sources alongside
DESIGN.md. The standalone `### Register` H3 stays for the cascade rules
(task cue → surface → register field).

New LLM-backed test suite at tests/skill-behavior/ runs five scenarios
against claude-haiku-4-5, gpt-5.4-mini, and gemini-3.1-flash-lite via
Vercel AI SDK. Captures real tool traces, asserts on context.mjs calls,
brand.md loads, and teach.md fallback. Skips cleanly when API keys are
unset. 13-14/15 pass; only stable failure is the v3.2.0-era gpt-mini S4
"don't re-run" regression. Adds @ai-sdk/google as devDep and the
test:skill-behavior npm script.

Touches em-dashes in skill/SKILL.md and four reference files so
`bun run build:skills` passes its skill-prose validator. teach.md and
document.md drop their "re-run the loader to refresh session cache"
steps since the agent's own write is now the freshest source.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* skill: merge orphan reference files into command sub-skills + inline S-tier invariants

Two related restructurings:

1. SKILL.md now carries the cross-domain invariants that catch defects in any
   project (contrast/placeholder/gray-on-color, similar-font pairing, text-wrap,
   tabular-nums, centered-stack default, Flex/Grid choice, auto-fit grids,
   semantic z-index, reduced motion, stagger vs section-fade, premium motion
   materials, focus-visible, placeholders-aren't-labels, dropdown overflow trap,
   button/link copy). Greenfield-only rules (theme picking, color strategy,
   tinted neutrals) live under "New projects only".

2. Reference files merged into their command counterparts:
   - spatial-design.md  -> layout.md
   - motion-design.md   -> animate.md
   - color-and-contrast.md -> colorize.md
   - responsive-design.md  -> adapt.md
   - ux-writing.md         -> clarify.md
   - typography.md         -> typeset.md (bolder.md redirected)
   - cognitive-load.md + heuristics-scoring.md + personas.md -> critique.md

   craft.md and shape.md "load references" lists updated to new file homes.
   interaction-design.md stays standalone (no 1:1 command verb).

Net: 36 -> 27 reference files. Same content, fewer files, no orphaned
reference loaded only from craft.md.

Also extends the routing rules: if the user's first word doesn't match a
command but the intent clearly maps to one, load that command's reference
and proceed as if invoked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* skill: add sub-command + existing-project scenarios; move sub-command load to step 2

Adds three new LLM-backed scenarios to tests/skill-behavior:
- S6: `/impeccable polish` → loads polish.md
- S7: `/impeccable audit` → loads audit.md
- S8: existing SvelteKit project (PRODUCT.md + DESIGN.md + src/app.css +
  src/lib/components/*.svelte + src/routes/+page.svelte) → agent reads
  at least one project code file to understand the existing design system

S6/S7 surface a real model-floor: gpt-5.4-mini reads brand.md, reads the
target index.html, and just does the polish/audit without ever loading
the sub-command reference. Stronger SKILL.md wording didn't move it.
Captured in the README baseline as a known weakness. Claude and Gemini
honor the load reliably.

To fix Gemini on S6/S7, sub-command reference loading is now Setup step 2
(right after context.mjs), not step 4 — placing it before the model gets
focused on "doing the work". Step 3 (design-system familiarization) is
tightened to require at least one project code read even when a
sub-command reference loads in step 2, so Claude doesn't laser-focus on
the sub-command flow and skip the broader exploration.

Two new fixtures: MINIMAL_LANDING_HTML (a tiny static landing page for
S6/S7) and SVELTE_PROJECT_FILES (a minimal SvelteKit scaffold with
tokens, components, and a routes/+page.svelte for S8). Both designed to
look real enough that agents treat them as production code.

Suite is now 24 tests across three providers; baseline is 21-22/24, with
the stable failures being gpt-5.4-mini scenarios 6 and 7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* skill: add reveal-animation safety rule (must enhance, not gate visibility)

Class-triggered visibility transitions pause on hidden tabs and headless
renderers. The italian-restaurant smoke produced a build where 2 sections
shipped opacity:0 because the CSS transition never advanced past
currentTime=0 (timeline paused). Added one-liner under Motion to prevent
the antipattern: reveals must enhance an already-visible default, never
gate content visibility on a class-triggered transition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* skill: restore prescriptive cream/sand/beige paragraph

Bisection across 5 historical skill commits on Gemini 3.5 flash fast
lane n=3 found that 0cf2debd was the peak quality state. The regression
between 0cf2debd and HEAD came from simplifying the long anti-cream
paragraph into a one-liner.

Restoring the paragraph (with em-dashes replaced by parens to satisfy
prose lint) recovers ~0.22pt average on Gemini vs HEAD, with the
largest gains on:
- 09-luxury-hotel: +0.50 (restores editorial drama in photo-led briefs)
- 10-food-magazine: +0.67
- 03-italian-restaurant: +0.51

The paragraph's load-bearing parts are the (a)(b)(c) alternatives that
give the model actionable replacements for cream-tinted body bg
("saturated brand color as body", "true off-white at chroma 0",
"darker mid-tone tinted neutral"). Without them, the one-line warning
left the model with no concrete alternative.

Cross-provider validation showed the pattern matches historical
behavior: Gemini benefits from prescriptive scaffold (+0.12 over off),
Sonnet is roughly neutral (+0.01), GPT-5.5 slightly regresses (-0.11
matching the v3.1.0 pattern of -0.11). The skill has never been
uniformly better than skill-off across providers; this is the closest
achievable state without provider-specific rework.

The structural improvements from the prior restructure stay (file
merges, S-tier inlines, routing rule extension, reveal-animation
safety rule).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: teach CLAUDE.md / AGENTS.md / DEVELOP.md about the skill-behavior tests

Adds the `bun run test:skill-behavior` script to the test commands lists
in all three docs. CLAUDE.md gets a full `### Skill-behavior tests`
subsection paralleling the existing Live-mode E2E one: how the suite
works (inlines source SKILL.md, scoped tools, asserts on the trace),
which providers it always runs (claude-haiku-4-5, gpt-5.4-mini,
gemini-3.1-flash-lite — all three every run), the eight scenarios, the
baseline (21-22/24 with stable gpt-mini sub-command-routing failures),
auth via repo-root `.env`, and how to add a scenario.

AGENTS.md gets the one-liner plus a paragraph in Testing Guidelines that
points contributors at the suite for Setup-touching edits (SKILL.md
Setup section, context.mjs, teach.md, document.md, register / sub-command
refs).

DEVELOP.md gets a short Testing section that didn't exist before, plus a
nudge in the "Test across providers" bullet pointing at the new suite as
the automated way to do that.

No code changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* detector: add 5 new antipatterns (em-dash-overuse, broken-image, marketing-buzzword, numbered-section-markers, aphoristic-cadence)

Consolidates eval-side detection logic into the canonical impeccable
detector. Before this change, the eval harness had its own duplicate
implementations of em-dash, copy-slop, and broken-image checks. They
now live alongside the existing 28 antipatterns in the impeccable
registry, available to the CLI, browser extension, critique skill,
and eval (via the existing slop grader child-process call).

New antipatterns:
- em-dash-overuse: 5+ em-dashes in body text content (threshold
  permits legitimate prose use of em-dash; only triggers on AI
  cadence-level density)
- broken-image: <img> with empty src, missing src, or src="#"
- marketing-buzzword: SaaS phrase list (streamline / empower /
  supercharge / enterprise-grade / cutting-edge / etc)
- numbered-section-markers: repeated 01 / 02 / 03 sequence as
  section labels — the AI editorial scaffold one tier deeper than
  tracked eyebrow chips
- aphoristic-cadence: 3+ manufactured-contrast ("Not a X. A Y.")
  or short-rebuttal ("Sentence. No clause." / "Sentence. Just
  clause.") constructions in body text

Engine wiring:
- broken-image runs as a static-html element rule (selector: img)
  and a fallback regex matcher (for non-HTML files)
- em-dash / buzzword / numbered / aphoristic run as regex
  page-analyzers, factored into a new runTextContentAnalyzers()
  helper that both detectText (non-HTML) and detectHtml (HTML)
  call, so .html files get the same coverage as .css/.tsx

Tests: 166 detector + 12 browser + 24 fixture all pass.
Browser detector rebuilt (162.7 KB).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* skill: drop unvalidated anti-centering rule; add image-led hero carve-out

The anti-centering rule ("Don't default to centering everything") was
added without empirical support. We have a detector for it
(everything-centered, threshold ≥70%) that fires on 0 / 998 samples
in the corpus — never validated, never useful.

Meanwhile the rule was almost certainly responsible for collapsing
Gemini 3.5 flash's luxury-hotel skill-on output from the canonical
"full-bleed photo + centered overlay headline" cinematic hero (the
shape skill-off Gemini chooses 67% of the time) to a 50/50
magazine grid (full-bleed rate drops to 18% under skill-on, -49pp).

Changes:
- skill/SKILL.md #### Layout: drop "Don't default to centering..."
- skill/reference/brand.md ## Layout: drop the same rule; replace
  with a positive carve-out — image-led briefs (hotels, restaurants,
  magazines, photography) often want full-bleed hero with overlaid
  menu and centered headline; let the photograph be the design
- skill/reference/layout.md: drop the assessment question and the
  "asymmetric breaks centered-content pattern" framing

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Apply neo-kinpaku design system and improve live picker UX

Restyle the live picker to match the site kinpaku kit, persist pick mode
in localStorage, fix DESIGN.md color swatches in the parser, and land the
neo-kinpaku site refresh with new tokens, assets, palette script, and
detector rules.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add live Steer end-to-end: poll protocol, browser UI, and E2E harness.

Wire page-level Steer through the live server and agent poll loop with steer_done
unlock semantics, extend live.md for agents, and add smoke tests with LLM
handleSteer plus recovery for hidden heroes, HMR lag, and dev-tool overlays.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add experimental live-poll --stream mode; keep one-shot default for Cursor.

Stream keeps one process alive with ack-aware resume, but live.md documents
that Cursor should stay on one-shot background notify after testing showed
~5s pickup vs sub-second on exit-based notify.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Sync harness output and fix build validators for poll stream release.

Regenerate provider skills after live-poll --stream work, update homepage
detection counts to 41, and replace em dashes in site/skill copy so
bun run build passes prose and count checks.

Co-authored-by: Cursor <cursoragent@cursor.com>

* homepage: add testimonials marquee section

A two-row testimonial marquee on a tinted graphite plinth, sitting
between the hero and the slop teaser.

29 testimonials sourced via api.fxtwitter.com (lightly cleaned: leading
@-mention reply targets stripped, trailing self-links removed). Avatars
downloaded into site/public/assets/testimonials/ so they're served
locally. Quote order curated for impact — both rows lead with the
punchiest quotes (Ben Davis spotlight, "Impeccable > Claude design",
"THIS. This shit works.", "Uninstall whatever frontend skill you're
using.") so the first viewport is loaded with the most memorable
testimonials.

Engineering notes:
- Section uses width:100vw + margin-left:calc(50% - 50vw) to escape
  main.site-content's max-width + side padding (cards now clip cleanly
  at the actual viewport edges).
- Marquee runs at 110s linear infinite. Both rows share the same
  duration so on-screen speeds match; track is doubled so the loop
  back to 0 reads as continuous.
- Hero min-height reduced from 100svh to calc(100svh - 115px) so the
  dotted divider and top of row A peek above the fold on landing,
  signalling the section is there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* homepage: keep the hero demo clear of the fixed header on short viewports

The hero centers its content in the full viewport (the site header is a fixed
overlay), so on shorter screens the tall Live Mode demo tucked under the nav.
Raise the hero's top padding above the 97px header (113px wide, 108/92px when
stacked) so content always pins below the header while still centering on tall
viewports, and cap the demo frame to the viewport so the whole demo stays on
screen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add steer voice input and refine processing animation.

Wire Web Speech API on the Steer mic with auto-submit, block Cursor's preview browser with a clear message, and replace truncated "Working" text with a dots-only processing state.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add agent poll connectivity indicator and tighten global bar spacing.

Surface poller state on the Impeccable mark via SSE and /status, with an instant disconnected tooltip, steer timeout failsafe, and matched brand/chat section gaps.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix steer focus to allow page text selection without losing type-to-steer.

Blur the hidden steer input on page interaction, pause refocus during selection gestures, and reschedule focus recovery after clicks and cleared selections.

Co-authored-by: Cursor <cursoragent@cursor.com>

* site: rework "Design in production" section glyphs and audience band

Put the three how-it-works steps back into thin-line cards and drop the
overused browser-chrome bars from each glyph. Redraw the step 2 and 3
visuals to mirror the real Live Mode UI: step 2 shows the on-canvas pick
outline with an attached comment bubble, step 3 shows the floating
contextual accept bar plus the source-write confirmation. Re-treat the
audience tiles as verdigris-lined text (no card box) under a "Who it's
for" eyebrow, so each role reads as distinct from the gold step band.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add live insert mode with HMR-safe placeholder recovery.

Ships insert picking, scaffold helpers, variant cycling fixes for hidden
variants, and placeholder snapshot/recreation so Astro HMR does not drop
the wait-state box or re-anchor to the hero container.

Co-authored-by: Cursor <cursoragent@cursor.com>

* site: mobile pass — hamburger nav + designing hero overflow fix

The header was rendering inline nav links + GitHub button that overflowed
narrow viewports (~363px). Pre-existing display:none hacks hid Designing
and Live to make the row fit, but those items still belonged in the menu.

Header.astro: added a hamburger toggle button + inline script. The right
cluster (nav + GitHub) becomes a collapsible drawer below the header on
mobile, with data-nav-open driving the open/closed state and animating
the two-line glyph into an X.

kinpaku-kit.css: hamburger button (kinpaku-bordered glyph), mobile drawer
panel (solid lacquer-deep bg, hairline separators between rows, full-width
tappable rows), and overrides for the older sub-pages.css mobile rules
(horizontal-scroll mask on the nav, hidden [data-nav="home"] item, hidden
GitHub star label) — all redundant now that the drawer surfaces everything.

home-kinpaku.css: dropped the @media (max-width: 560px) block that hid
Designing / Live / GitHub. The drawer pattern shows them all.

designing-kinpaku.css: hero h1 "Designing with Impeccable" was overflowing
at narrow viewports. Three fixes:
  - grid-template-columns 1fr → minmax(0, 1fr) so the column shrinks to
    fit container instead of growing to "Impeccable"'s 472px intrinsic
    min-content width.
  - mobile h1 size override (clamp(2.2rem, 11vw, 3rem) at <=480px) since
    the display token's 3.4rem minimum is sized for desktop hero impact.
  - hide the decorative loop-wheel SVG below 600px (was overflowing 22px
    past the right edge).

Verified clean at both 363px and 403px viewports across /, /docs,
/docs/animate, /slop, /designing, /live-mode. scrollWidth matches viewport
width on every page (no horizontal scroll).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* detector: refine new rules + run provider tells in browser env

Follow-up to the detector port (rules landed in 7648af00):
- oversized-h1: flag long headlines set at display size, not punchy
  one/two-word heroes (length, not size alone, is the tell)
- provider tells (--gpt/--gemini) now always run in a real browser env
  (detector page, live overlay, extension); gating is a CLI-output
  concern only, applied in the Node engine return paths
- move theater-slop-phrase into checkHtmlPatterns so it runs in the
  bundled browser path, not just CLI/static (browser bundle excludes
  detect-text.mjs)
- hero-eyebrow-chip overlay highlights the eyebrow, not the heading
- gemini-tells fixture: data-URI images so the hover-zoom renders
- rebuild browser bundle

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* site: migrate /detector lab to neo-kinpaku design system

Rebuild the detector lab tool shell on --ks-* tokens (lacquer ground,
gold hairlines, champagne/mono type) instead of the legacy warm-paper
palette. Swap the "/" placeholder for the real carved-tile brand lockup,
restyle the toolbar actions as kinpaku primary/secondary buttons, and
recolor the finding overlay from off-brand magenta to vermilion.

Update the global theme-color from #fafafa to #010101 (the sRGB render
of the lacquer ground) so the browser chrome matches the dark site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Homepage: hero finalist, compact live demo, real picker bar.

Switch the hero to m-01-v2-01, tighten the in-hero demo layout, and replace
the marketing gbar with a shared LiveDemoGbar that mirrors live-browser.js.
Size the bar with max-content so controls are not clipped inside the capsule.

Co-authored-by: Cursor <cursoragent@cursor.com>

* site: migrate /cases/neo-mirai to neo-kinpaku design system

Rebuild the Neo Mirai case-study page on --ks-* tokens: lacquer ground
(drops the off-brand magenta radial spotlight), Alumni Sans Pinstripe
display headings instead of the banned italic serif, gold eyebrow/labels,
gold hairline image frames, kinpaku primary/secondary buttons, and a
lacquer-deep command panel with a gold-bordered code block.

Opt .neon-case-page into the shared kinpaku site-header/footer chrome in
kinpaku-kit.css (per the "add new kinpaku pages to the selector list"
note) so the global header and footer go dark to match the page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* site: consolidate kinpaku header+footer into one reusable .kinpaku-chrome class

The dark header/footer were not a reusable unit: the header was scoped to
a per-page selector list, the github star pill was home-only, and the
default footer was copy-pasted into four page stylesheets. Pages not on
the lists (like /cases/neo-mirai) fell back to the legacy light chrome.

Collapse all of it into one `.kinpaku-chrome` block in kinpaku-kit.css —
header, github pill, and default footer — and opt every kinpaku page in
via a single body class. Delete the four duplicated per-page footer
blocks and the home-only github pill. The home page keeps its textured
verdigris footer as a deliberate override, raised to body.home-kinpaku
specificity so it wins regardless of import order. Genuinely light pages
(privacy, tutorials) just omit the class.

Fixes on /cases/neo-mirai: footer and github star now render dark/kinpaku
(were legacy-light), and the content sections are wrapped in the .neon-case
container so they sit in header-aligned gutters instead of bleeding to the
viewport edge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* site: migrate privacy + tutorials to kinpaku via a reusable surface class

These were the last two light pages. Rather than rewrite their per-rule
styling, add a reusable .kinpaku-surface class that remaps the legacy
--color-* / --font-* tokens to kinpaku values at the body scope, so the
existing legacy-token CSS (sub-pages.css prose, the pages' inline styles)
renders dark for free. Same trick docs-kinpaku/slop-kinpaku use per page,
lifted into one shared class. Pair it with .kinpaku-chrome for header +
footer.

privacy + both tutorials pages now carry both classes. Also force the
sub-1.2rem headings (tutorial card titles, prose h1/h2) back to the
upright body face: the legacy display face was italic serif, and the
kinpaku Pinstripe face reads wrong synthesized-italic at small sizes.

No light pages remain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* site: re-add Tutorials to the /docs sidebar

Tutorials lost its docs placement across two refactors: the Astro docs
rebuild never carried over the sidebar tutorials list the old generated
pages had, and the kinpaku homepage redesign dropped the "Full
walkthrough" link. It survived only via /designing and /live-mode.

Add a "Tutorials" group at the top of the docs sidebar (matching the
command-category styling) linking the index plus all four tutorials,
restoring the old information architecture.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* site: make kinpaku the default — flip legacy :root tokens to dark (phase 1)

Repoint the legacy design tokens in tokens.css from light-mode to kinpaku:
--font-* now reference the --ks-* brand faces (retiring Cormorant/Instrument/
Space Grotesk), surfaces carry dark-lacquer oklch, and --color-accent is gold
instead of magenta. Values mirror the per-page kinpaku remaps.

Every live page already overrides these at its body-class scope, so this
changes the fallback (any classless/new page now renders kinpaku) without
altering existing pages — verified home, designing, slop, live-mode, docs
unchanged, and the deliberate-light demos (slop specimens, home's Aurelia
mock) still render light via their own colors.

First step toward removing the per-page remaps; those become redundant next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* detector + slop: cream-palette rule, drop everything-centered, polish catalog

- new deterministic cream-palette rule ("claude beige"): flags warm
  lightly-tinted off-white page backgrounds; wired into static + browser
  engines, with fixture + test
- remove everything-centered rule entirely (no longer in the skill) from
  registry, regex analyzer (+ index-offset fix), checkPageLayout, and tests
- catch Instrument Serif in overused-font (regex + OVERUSED_FONTS)
- /slop: reconcile catalog (cream card in, everything-centered out; counts),
  and fix demo visuals — visible hairline border, gigantic clipped hero,
  more extreme crushed tracking, padded gray-on-color card, uniform-rhythm
  monotonous-spacing, long line-length line, elastic-overshoot dialog for
  bounce easing, real zooming image for image-hover; flip the demo surface
  off warm beige to a cool neutral

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* detector page: add cream-palette fixture to the catalog

Surfaces the new cream/beige palette rule on /detector alongside the
other Color specimens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* site: shared docs sidebar + tutorial pages join the layout

Extract the /docs section sidebar into a reusable DocsSidebar component
and wire it into all three entry points so the navigation is consistent
across docs index, command pages, and tutorial pages.

site/components/DocsSidebar.astro (new): one source of truth. Loads the
tutorials + skills collections, renders Tutorials → Commands grouped by
category, and highlights the active entry via activeCommand / activeTutorial
props.

site/pages/docs/index.astro: swap the inline sidebar markup for the
component. Drop the "All tutorials" link — the dedicated tutorials
listing page wasn't earning its slot in the rail.

site/layouts/Doc.astro: same swap. Command pages now also see the
Tutorials section above Commands, matching /docs.

site/pages/tutorials/[...slug].astro: rewrite from a standalone page
(custom .tutorial-page wrapper, ad-hoc breadcrumb) to the full
skills-layout shell with DocsSidebar in the left rail. Tutorial content
now reads in the same layout as command reference pages.

site/content/tutorials/brand-vs-product.md (deleted): the skill picks
the register automatically from PRODUCT.md, so a tutorial telling users
to pick it themselves was misleading.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* detector: catch Tailwind warm-light bg utilities in cream-palette

The static engine can't resolve Tailwind classes to computed CSS, so a
`bg-amber-50` on <body> slipped past the cream-palette rule. Add a
class-list fallback that scans body/html for arbitrary `bg-[...]` values
and named warm-light utilities (amber/orange/yellow/stone), each run
through the same isCreamColor test so neutrals and over-saturated shades
drop out. Fixture + test for the class-only case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* site: drop redundant per-page token remaps (phase 2)

With kinpaku now the :root default, the --color-* / --font-* remap blocks
in docs/slop/designing/live-mode-kinpaku.css re-declared values identical
to :root. Removed them, keeping only the --ks-muted alias (still read by
name in those files) and each page's shell (gradient bg, color, min-height).

home-kinpaku.css keeps its remap: it uses home-specific values (e.g.
--color-charcoal: var(--ks-text), --color-cream: var(--ks-lacquer-raised))
plus the --cat-* gradient overrides, so it is not redundant.

Verified designing (PRODUCT.md viz), slop (specimens stay light), docs,
live-mode unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* site: drop italic from 15 dead editorial-serif heading rules

Audited every font-style: italic in sub-pages.css and main.css against
the live markup. Removed italic from the 15 rules whose selectors don't
appear in any page/component/content/script:

  sub-pages.css: docs-home-card-title, docs-category-title,
    tutorial-embed-caption, skill-demo-caption, skill-source-card-subtitle,
    skill-references-heading, skill-reference-title
  main.css: hero-title-combined, hero-tagline-combined, impeccable-title,
    loading-state, install-primary-howto .install-path-desc em,
    install-howto-steps > li::before, install-step-status, consulting-title

These were dormant remnants of the retired Cormorant italic-serif look —
the kinpaku Pinstripe face renders them as bad synthesized-italic, but
no markup matches the selectors so nothing rendered. Removed only the
font-style declaration; the rest of each rule stays (whole-rule cleanup
is out of scope).

Kept the 5 live selectors (slop-section-heading, tutorial-card-title,
visual-mode-demo-caption, visual-mode-method-name, gallery-card-title)
per the "if they're not used anywhere" condition, plus .prose em (real
emphasis) and .prose blockquote (conventional blockquote italic).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* skill: brand-seed palette.mjs + Setup step to run it

New-brand color now starts from a curated seed color (129 OKLCH seeds)
instead of the model guessing or defaulting to warm-cream. The script
returns one seed + composition guidance (pure-bg architecture, perceptual
text-on-fill, anti-cliché moods, jewel-tone range), with inverse-frequency
hue weighting for fair rainbow exposure and deterministic --from picking.
SKILL.md Setup step 5 makes it run for greenfield projects. Curation
tooling lives in the impeccable-evals repo (tools/palette/).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Remove accidental live mode inject from Base.astro.

The localhost live.js tag was left in the site layout after a dev session and should never ship in the Astro template.

Co-authored-by: Cursor <cursoragent@cursor.com>

* site: dedicated /changelog + /faq, epic v3.5.0 notes, Live Mode → Beta

Split changelog and FAQ out of the homepage into two standalone kinpaku
pages, linked from the footer (and a quiet hint under the Get-started CTA).

/changelog: every release inline (no collapsible), newest first. The
v3.5.0 entry leads with a one-line summary, a real before/after pair from
the GPT-5.5 eval corpus (luxury-hotel brief, skill off vs on), and a stat
row (74% cream-bg, 76% extreme tracking, 90%+ low-contrast — measured
across ~190 samples). Then five scannable bold-led bullets, biggest
takeaway first: per-provider skill compilation, the bias-fix, Live Mode,
the 7 new detector rules, the tighter skill. Before/after JPGs optimized
to ~470KB total (down from ~2.5MB PNGs).

/faq: the six support questions, each deep-linkable.

Live Mode is now Beta everywhere it surfaces: the /live-mode eyebrow
badge and note, the homepage bento tile badge, and the changelog entry.
The historical v3.0 changelog entry stays "Alpha" — accurate to what
shipped then.

Footer trimmed to the four links not already in the top nav (Changelog,
FAQ, Privacy, GitHub).

Version bumped 3.2.0 → 3.5.0 across the three plugin manifests; the
3.2 bias-fix work folds into this release rather than shipping separately.

astro.config.mjs: disable the dev toolbar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* site: point /design-system hero at the m-01-v2-01 finalist

design-system.css referenced kintsugi-hero-v2.png, an untracked orphan
that was never committed. Repoint it at the committed m-01-v2-01 finalist
so /design-system and the homepage hero share one image, and the page
no longer depends on a file outside the repo. The v2 orphan moved to tmp/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build: sync harness mirrors + green the prose gate

Rebuild propagates the committed skill source (palette.mjs Setup step,
detector rule updates, brand.md) into the 13 harness output dirs and the
plugin subtree, which had drifted from source.

Also fixes the prose validator, which had been red on six pre-existing
hits across committed files:
- Four em dashes in code comments (Testimonials.astro, LiveDemoGbar.astro,
  index.astro) and one in skill/reference/live.md — reworded to colons/commas.
- Two in the slop catalog (an em-dash-overuse specimen and the
  marketing-buzzword rule naming "empower"). Those are intentional: the
  slop page documents every antipattern by example, so it must contain
  them. Exempted site/pages/slop from validateProse rather than neutering
  the specimens.

`bun run build` is now green end to end: counts validate, prose passes,
site builds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* skill: rewrite no-section-fade rule to fix Gemini zero-motion overcorrection

The old rule ("whole-section fade-on-scroll is the saturated AI motion
reflex") drove Gemini to overcorrect into shipping pages with no motion
at all: motion-variety 39% / zero-motion 12% with the skill on, vs
~74-78% variety and ~3% zero-motion without it.

Rewrite keeps the legitimate-stagger carve-out, names the defect at
shape level (one identical entrance on every section) without
enumerating motion primitives, and adds an explicit clause that
suppressing the reflex is never grounds for a static page.

Validated on Gemini 3.5-flash (n=10, luxury-hotel + infra-platform):
motion-variety 39% -> 70%, zero-motion 12% -> 0%, staggered-reveal
stays 0% (reflex not re-inflated).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* release: bump CLI to 2.2.0 and extension to 1.1.0

Both ship the expanded detector: the 7 new rules (cream-palette,
em-dash-overuse, marketing-buzzword, numbered-section-markers,
aphoristic-cadence, broken-image, italic-serif-display) plus
hero-eyebrow-chip, with everything-centered removed. 41 rules total.

The extension settings page already supports toggling them: the rule
list renders from detector/antipatterns.json, grouped by category, and
disabledRules flows through chrome.storage.sync into the scan config,
which detect.js honors by rule id. New rules are toggleable with no UI
change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* release: fix release.mjs for the moved changelog + add CLI/ext entries

The changelog moved from site/pages/index.astro to its own
site/pages/changelog.astro with new markup (cf-version / cf-entry /
cf-items), which left release.mjs reading the wrong file with the old
selectors. All three release commands would have failed at note
extraction. Point it at changelog.astro, match cf-version, and scope
notes to the <ul class="cf-items"> bullet list — that also skips the
lead paragraph, before/after figure, and stat row on the v3.5.0 entry,
keeping release notes to clean bullets.

Add CLI v2.2.0 and Extension v1.1.0 changelog entries (the shared
detector update: 7 new rules, everything-centered removed, 41 total;
plus the extension's per-rule toggles) so release:cli and release:ext
have notes to extract.

Verified extraction for all three labels: v3.5.0 (5 bullets),
CLI v2.2.0 (3), Extension v1.1.0 (2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: correct dev server port to 4321 and drop stale pnpm-lock

Astro serves on 4321, not 3000 as the docs claimed; update CLAUDE.md,
AGENTS.md, and screenshot-antipatterns.js. Remove the leftover
pnpm-lock.yaml from the Astro migration so Cloudflare's frozen install
uses the maintained, in-sync bun.lock instead of a drifted pnpm lockfile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* site: rework /designing flow, rhythm, Live Mode mock, and CTA

Restructure the page so iteration reads as the core value, not net-new.
The four loop phases are wrapped in a track with a sticky scroll-spy nav
(Start/Iterate/Polish/Maintain) that pins under the header and highlights
the active phase; the surfaces section (skill/CLI/extension) moves out of
the loop into the post-loop context group so the loop runs uninterrupted.

Fix the iterate split: shared subgrid row tracks so the terminal and the
Live Mode mock align on the same baseline regardless of paragraph length,
wider intro measure (52ch, was a crammed 36ch), and a deeper picker stage
so the context and global bars breathe instead of stacking on the card.

Rebuild the Live Mode mock to mirror the real picker: carved-tile mark plus
Pick / Insert / Detect / DESIGN.md controls on lacquer-deep with the gold
border, and a /impeccable live entry line so the reader knows how to start.

Reframe Start as the hard mode, move h3 subheads off the thin display face
onto Albert Sans, and trim Start so it no longer dominates the loop.

Rework the closing CTA into two standalone raised cards (the bento plinth
made them read as boxes nested in a box), and fix the tutorials copy: there
are three walkthroughs now, and the brand-vs-product tutorial is gone, so
drop it from the CTA and remove the dead lane link to it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* site: reorder Get Started so usage follows setup, link out to more

Move the /impeccable usage examples below the Chrome extension, CLI, and
Stay-updated block. Running a command is the logical next step once the
skill, extension, CLI, and subscriptions are all in place, so the section
now reads install -> set up the extras -> use it. Add a closing "Go deeper"
line linking to the Designing with Impeccable workflow page and the docs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: install compiled per-provider skill variants, not uncompiled source

`npx skills add` (and `impeccable skills install`, which wrapped it) installed
the uncompiled skill/ source verbatim: the skills CLI dedupes discovery by name
and picks skill/SKILL.md first, so installs shipped unresolved {{placeholders}}
and no vendored detector (#168).

- Rename skill/SKILL.md -> skill/SKILL.src.md so the skills CLI's discovery
  skips the source and falls through to a compiled .agents variant; update the
  build reader, skill-behavior harness, and docs to match.
- Refactor `impeccable skills install` to copy each harness's compiled variant
  from the universal bundle (real dirs, no npx skills, no symlink), with
  project/global harness detection and a --providers override.
- Fix stale unit tests (replacePlaceholders, readPatterns, transformer
  prefix/summary) that asserted removed pre-v3.0 behavior, and wire the three
  orphaned test files into `bun run test` so the drift can't recur.
- Split skills-cli.test.js: pure blocks run by default, network blocks move
  behind a new `bun run test:cli-e2e`; fix its stale update assertions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* site: default to `npx impeccable skills install`, restore install-method panel

Get Started recommended `npx skills add`, which installs a single shared build
across harnesses. Make our CLI the default (it installs the build compiled for
each harness) and bring back the "Other install methods" disclosure the
neo-kinpaku redesign dropped.

- Homepage: primary command is now `npx impeccable skills install`; a native
  <details> panel offers the Claude Code plugin and `npx skills` (caveated as
  installing one shared build rather than the per-harness one).
- FAQ: recommend `npx impeccable skills install` to install, `--force` to
  reinstall, and note the npx skills shared-build caveat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* site: reword craft tagline so it doesn't lead with "Shape"

The craft card's tagline began with the word "Shape", which reads like
the name of the sibling /shape command and made the two cards look
swapped (#166). Reword to "Design it, then build it, all in one flow."
No data was actually swapped; this is a copy collision fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* skill: rename teach -> init and expand its setup flow

Rename the `/impeccable teach` command to `/impeccable init` across the
skill, site, CLI, and tests. `teach` stays as a deprecated router alias and
/docs/teach + /skills/teach redirect to /docs/init.

Expand the command beyond writing PRODUCT.md/DESIGN.md: the same codebase
crawl now also pre-configures `.impeccable/live/config.json` (Step 6, with
CSP consent) so live mode boots with no first-time detour, and the flow ends
by recommending the best commands to run next from what the scan surfaced
(Step 7).

Fold two items into the unreleased v3.5.0 changelog entry: the init rename
and the brand-seed palette picker. No version bump.

Regenerates all harness skill output dirs and the _redirects file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: lead README install + usage with the CLI installer

Add `npx impeccable skills install` as the recommended install option and
update the Usage section to the `/impeccable <command>` form, dropping the
nonexistent `/normalize` example.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(skill-behavior): swap to production-tier models (sonnet + gpt-5.5)

Replace the cheap-tier default lineup (claude-haiku-4-5, gpt-5.4-mini) with
production-tier models (claude-sonnet-4-6, gpt-5.5) so the skill-behavior
suite reflects what users actually run. gemini stays on flash-lite.

Sync the docs (CLAUDE.md, AGENTS.md, tests/skill-behavior/README.md): new
model names, cost estimate raised to ~$0.50-1.50/sweep, and the old 21-22/24
baseline reframed as previous-cheap-tier history pending re-measurement on
the new lineup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: self-updating skill via boot-time version check

context.mjs now polls a new lightweight /api/version endpoint at most once
per day (cached globally in ~/.impeccable) and appends an UPDATE_AVAILABLE
directive when a newer skill version has shipped, prompting the agent to
offer `npx impeccable skills update`. Best-effort and silent on any failure;
asks before updating; suppresses re-prompts for a declined version for a
week. Opt out with IMPECCABLE_NO_UPDATE_CHECK=1.

- skill/scripts/context.mjs: version read, throttle + anti-nag cache, directive
- scripts/build.js + _redirects: /api/version endpoint (from plugin.json version)
- skill/SKILL.src.md: document the UPDATE_AVAILABLE boot branch
- tests/context.test.mjs: coverage for cached/newer/suppressed/opt-out paths
- changelog: v3.5.0 entry
- synced harness skill dirs via bun run build

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: cover the self-update path (network + LLM behavior)

context.test.mjs: add a localhost stub-server integration test for the live
fetch path (poll /api/version, cache a newer version, stay silent on
same-or-older, fail silent + stamp lastCheck when unreachable). Runs against
127.0.0.1 only, never the real site; uses async spawn so the in-process stub
isn't deadlocked by spawnSync blocking the event loop.

skill-behavior: add scenario 9 asserting the agent surfaces UPDATE_AVAILABLE
but never auto-runs `npx impeccable skills update` without asking. New
prepareWorkspace `skillVersion` copy-mode (so context.mjs has a SKILL.md to
version-check), env threading through runTurn -> execBash, and bash-output
capture to prove the agent actually received the directive. Passed on
claude-sonnet-4-6, gpt-5.5, and gemini-3.1-flash-lite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 14:22:22 -07:00
Paul Bakaus de9aa13a53 ignore talks 2026-05-14 15:42:44 -07:00
93a13f9882 Critique persistence: per-run snapshots, ignore list, polish reads as signal (#153)
* critique-storage: new helper for per-run snapshot persistence

Adds skill/scripts/critique-storage.mjs with:
- slugFromTarget(): mechanically derive a stable slug from a resolved
  file path or URL (NOT from the user's natural-language phrasing),
  so the same target lands in the same stream across runs even when
  dev-server ports drift or the user phrases it differently.
- writeSnapshot(): writes .impeccable/critique/<timestamp>__<slug>.md
  with a small YAML frontmatter (timestamp, slug, target, total_score,
  p0_count, p1_count) plus the report body.
- readLatestSnapshot(): newest snapshot for a slug, used by polish.
- readTrend(): last N frontmatter entries for a slug, used by critique
  to print the score trend line.
- readIgnoreList(): non-empty non-comment lines from ignore.md, the
  ONLY input critique consumes from prior runs.

No separate index.json. The snapshot files are the single source of
truth; trend reader globs them and parses frontmatter. Deleting a
snapshot removes it from the trend cleanly with no orphan rows.

CRITIQUE_DIR constant + getCritiqueDir / getCritiqueIgnorePath added
to impeccable-paths.mjs alongside the existing live-dir helpers.

19 unit tests in tests/critique-storage.test.mjs cover slug stability,
URL and file inputs, round-trip read/write, trend filtering by slug,
and ignore-list parsing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* critique: persist snapshot per run, respect ignore.md

Two new steps wired into the critique flow:

- Setup: Resolve Target and Load Ignore List. Before gathering
  assessments, resolve the user's natural-language target ("the
  homepage") to a concrete artifact, compute the slug via
  critique-storage.mjs, and read ignore.md. Matching findings drop
  silently from the report. This is the only prior-run input
  critique consumes; anchoring on prior findings would defeat
  independent assessment.

- Persist the Snapshot. After the report is finalized (before Ask
  the User), write it to .impeccable/critique/<ts>__<slug>.md with
  structured frontmatter, then surface a one-line trend ("Trend for
  index-astro: 24 → 28 → 32") and the written path. First run says
  "no trend yet". Persistence is fire-and-forget; failures print and
  move on rather than blocking the rest of the flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* polish: read latest matching critique as fix backlog

When polish is invoked after critique on the same target, the
critique's P0/P1 findings are the right backlog; don't re-derive
them.

Adds a Setup step that resolves the target, computes the slug via
critique-storage.mjs slug, and reads the latest matching snapshot
via critique-storage.mjs latest. Found → use those P0/P1 items as
the polish backlog and mention the snapshot path. Not found →
proceed independently from a clean slate.

Explicitly does NOT read snapshots for other targets (cross-target
context is pollution). Explicitly does NOT cascade to atomic moves
(bolder, quieter, clarify, animate, etc.); those act on a specific
selection where the page-level critique would be noise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* gitignore: .impeccable/critique/, opt ignore.md back in

Per-run critique snapshots are local artifacts (same precedent as
.impeccable/live/sessions/), but ignore.md carries user-curated
deferrals that may be worth sharing across a team. Negate-pattern
keeps it trackable while the snapshot files stay local.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* polish: reframe prior critique as additional signal, not backlog

Three corrections to the previous polish.md addition:

- "Polish is usually invoked after critique" is wrong; people polish
  without ever running critique. Dropped the presumption.
- "This is the only command that auto-reads prior critique" leaks
  cross-command scope into polish's reference file. Dropped.
- Treating critique findings as THE polish backlog biased polish to
  only fix what critique flagged, skipping its own checklist. The
  critique is one input among many; fold its P0/P1 items into the
  polish list, then do the normal pass.

Now lives as a short item 4 in Pre-Polish Assessment ("Pull in any
prior critique — optional signal") instead of a top-level Setup
section. Less prominent, doesn't presume invocation order.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* critique-storage: drop the ignore subcommand, read ignore.md directly

The ignore-list helper did nothing the model can't do inline: read a
markdown file, skip blank and #-prefix lines. It added a tool
roundtrip for no real value. Other helpers earn their keep by doing
work the model can't trivially do (path normalization, filename
generation, glob + frontmatter parsing); ignore-list did not.

Removed:
- `ignore` CLI subcommand
- readIgnoreList() module export + its tests
- getCritiqueIgnorePath() from impeccable-paths.mjs (now dead code)

Critique.md step 3 now just says "read .impeccable/critique/ignore.md
if it exists" and explains the format inline. Simpler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* critique-storage: caller meta cannot override timestamp or slug

Spotted by Cursor Bugbot on the PR. writeSnapshot built frontmatter as
{ timestamp, slug, ...meta } so a caller-supplied meta blob (parsed
from the IMPECCABLE_CRITIQUE_META env var) could silently clobber the
computed timestamp and slug. The filename keeps the computed values,
so the frontmatter would drift from the filename and readTrend would
attribute scores to wrong timestamps with no visible error.

Swap to { ...meta, timestamp, slug } so internal values always win.
Add a regression test that passes corrupt meta and asserts the
frontmatter still matches the filename.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 00:06:02 -07:00
e7e923c4ef Skill + craft cleanup, detector hardening, native subagent pipeline (#152)
* skill: drop quality tiers, keep the real brand-craft guardrails

Codex's craft/brand pass introduced fast/ship/showpiece "quality bars"
plus brand-specific build gates, asset ledgers, sub-agent review, and
self-graded fallback labels. In practice those tiers became escape
hatches rather than craft pressure: the final output should always be
10/10, and the real decision points are splashiness and maximalism, not
quality.

Removed:
- All quality-bar / showpiece / fast / ship framing in shape.md and craft.md
- Standalone Brand Direction (#4) and Asset Requirements (#10) sections
  in shape's brief; renumbered back to 1-10
- The Brand hard rules section in brand.md (folded its real prohibitions
  into the existing Imagery and Brand bans sections)
- Brand-specific build-gate item, mock-fidelity bullet, production-bar
  bullet, present-step bullet in craft.md
- Asset ledger ceremony in craft Step 4
- Review-only sub-agents and "self-reviewed fallback, not independently
  validated" machinery in craft.md and polish.md
- The For brand surfaces, assess hard failures subsection in polish.md
  and the brand checklist row
- tests/brand-showpiece-reference.test.mjs (and its package.json wiring)

Kept (the real nuggets):
- Asset-substitution prohibition: image-led briefs ship real/generated
  assets or canvas/SVG/WebGL, not generic CSS panels, cards, bullets,
  or copy
- Repeated tiny uppercase tracked kicker labels as a brand ban
- Detector/QA output is defect evidence only, never proof of quality
- "What visual assets are real content here?" discovery question
- Inspect each major section individually for brand and long-form work
- repeated-section-kickers detection rule + fixture
- CLI improvements (JSON to stdout, -json/-fast aliases, severity field)
- critique.md: npx impeccable detect --json fix

Harness output dirs refreshed via bun run build. Full test suite (186)
passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* skill: strip gate ceremony; require shape pause; allow compact briefs

The setup gate table and IMPECCABLE_PREFLIGHT banner pushed every
craft run through ritual restatement (PRODUCT.md → original prompt
→ round 1 → round 2 → 70-line "confirmed brief" → critique → summary,
all saying the same thing). Replaced with imperative prose that still
demands the same work but skips the user-facing telemetry.

Specifically:

SKILL.md
- Drop the Setup gate table and IMPECCABLE_PREFLIGHT banner.
- Keep the imperative steps explicitly: load context, identify register
  and load brand.md or product.md, AND load the matching command
  reference (craft.md / shape.md / etc.) when a sub-command is invoked.
  The command-reference step is non-negotiable; without craft.md loaded
  the agent skips the shape-and-confirm pause.

craft.md
- Drop the Build Gate / Craft Contract formal sections; replace with
  one paragraph stating prerequisites.
- Step 1 explicitly requires ending the response after presenting the
  shape output; the user must confirm before any code lands. Allows a
  compact 3-5 bullet brief when the prompt + PRODUCT.md already pin
  direction (full 10-section structure reserved for genuinely
  ambiguous tasks).
- Step 3 image gate skips silently when image generation isn't
  natively available; no user-facing announcement.
- Step 6 explicitly legitimizes "first pass clean, shipping" as a
  valid endpoint and bans inventing fake defects to demonstrate
  iteration.

shape.md
- Cap discovery at 1 round by default; second round only when first
  leaves material gaps.
- Adds an "assert-then-confirm, not menu-with-escape" rule: when
  PRODUCT.md and the prompt make one option obvious, name it and ask
  for confirm or override instead of enumerating "Restrained /
  Committed / Or something else?" as a real choice.
- Phase 2 brief has two forms now: compact (default for clear briefs)
  and full structured (genuinely ambiguous). Open Questions can't
  double as leading-with-Recommend; if you'd write "Recommend: X",
  decide X.
- Image gate same as craft.md.

Validated end-to-end with a Haiku skill-on observability run: agent
loads craft.md plus the brief's recommended implementation refs,
pauses for one productive question (accent color, trace fidelity,
CTA), and ships an artifact with zero side-tab violations vs. the
original v1 baseline. Cost trades up modestly for that quality.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* craft.md Step 6: Reading the screenshot is the inspection, not taking it

A v4 eval run took 4 targeted screenshots (hero, mobile, tablet,
query-section) and then never Read any of them back. The agent treated
browser_screenshot itself as "I inspected" and shipped without the
multimodal feedback loop ever closing. Detector caught the resulting
slop (5+ side-tab violations) on adjacent runs that did the same thing.

Step 6 now spells out the pattern explicitly: take the screenshot,
then Read the resulting PNG so its image content enters the
conversation as multimodal input, then critique what you actually see
in the image. With a check: "if your critique could have been written
without looking at the image, you didn't look at the image."

Validated with v5b: agent took 6 screenshots, Read all 6 back, and
shipped with zero detector findings (vs the previous greenfield runs
that hit 1-12 findings each).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* craft + brand: framework foundation, build-pipeline respect, image verification

Three closely-linked additions surfaced by an eval-harness session
investigating why the agent always shipped flat single-file HTML and
zero imagery on greenfield brand briefs.

1. craft.md gains a new Step 0 "Project Foundation" before Shape.
   Detects existing framework / component library / icon set and
   uses what's there. Greenfield: ask the user via AskUserQuestion
   with sensible defaults framed by the brief (Astro for content/
   brand sites, SvelteKit/Next/Nuxt for app surfaces, single
   index.html only for one-shot demos). Skipping the framework
   decision and writing flat HTML "to satisfy the spec" produces
   work that reads as a 2018 prototype regardless of visual
   quality.

2. craft.md Step 5 production bar gains two bullets:
   - Respect the build pipeline. Edit source files and run the
     project's `npm run build`; do not write to build/ / dist/ /
     .next/ directly with cat/heredoc/Bash redirects. Bypassing
     the pipeline skips asset hashing, image optimization, code
     splitting, and CSS extraction.
   - Verify external image URLs before referencing them. Use an
     image-search MCP, web-fetch tool, or browser if available;
     guessed photo IDs ship as broken-image placeholders.

3. brand.md "Imagery" section:
   - Generalizes the Unsplash URL guidance to "verify URLs
     before referencing them" with a hierarchy: image-search MCP
     > web-fetch > confidence-restricted manual selection >
     fewer photos.
   - Tightens the tech/dev-tool exception. Old line "zero imagery
     can be correct" gave models a permission slip. New framing
     keeps the underlying truth (typography + code + diagrams
     primarily carry voice) but raises the floor: imagery still
     earns its place when it serves the brief, and skipping it
     requires naming the typographic/diagrammatic move that's
     carrying the visual weight instead. "Zero imagery is the
     failure mode of laziness, not restraint."

Eval-harness corpus that prompted this: 19/19 brand landing tasks
shipped 0 images each, including ones where Opus had taste enough
to break the dev-tool color default lane. The skill needs to teach
both halves of the decision; the harness shouldn't have to nudge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* detector: body-text-viewport-edge rule + OKLCH/var-resolution + anchor-inherit FP fixes

New rule: body-text-viewport-edge flags body paragraphs that render flush
against the left/right viewport edges (no container padding). Tested via
the new tests/fixtures/antipatterns/body-text-viewport-edge.html fixture
(3 flag cases, 5 pass cases) and the test in detect-antipatterns-browser.

False-positive class fixes — all jsdom-mode only (real browsers resolve
the cascade correctly so these gates stay inert there). Five related
gaps that compounded into ~14× spurious contrast findings on Tailwind v4
pages with OKLCH color tokens:

  • OKLCH parser. jsdom returns the literal "oklch(...)" string from
    getComputedStyle; the detector now converts to sRGB via Björn
    Ottosson's matrices. Handles Tailwind v4's compact minified form
    "oklch(21.5%.02 50)" (no space after %).
  • var() resolution. resolveBackground + checkElementColors now
    accept the existing customPropMap and parse `var(--color-paper)`
    etc. as proper RGB via the new parseColorResolved helper.
  • bg-color before bg-image. The old order bailed on any gradient
    ancestor before checking for a solid background-color underneath,
    causing the body's decorative paper-grain gradient to be measured
    against instead of the page's actual `bg-paper` cream.
  • body/html-level gradient → white fallback. When the only opaque
    ancestor we can read is body/html with a gradient overlay (and
    jsdom can't decompose `background: var(--paper) gradient` to
    extract the solid color), return white instead of falling through
    to resolveGradientStops — which was picking up paper-grain noise
    colors and using them as the bg.
  • Anchor-inherit workaround for jsdom :link UA specificity.
    Tailwind v4's preflight declares `a { color: inherit }` (0,0,1).
    jsdom's UA stylesheet has `:link { color: blue }` at (0,1,1) and
    wins the cascade. Real Chrome wraps :link in :where() (0,0,0) so
    the page rule wins. When the page declares the inherit rule AND
    we see jsdom's default `rgb(0,0,238)` on an anchor, walk to the
    nearest non-anchor ancestor and use its color.
  • Alpha-fallback safety gate. When text has alpha<1 AND we couldn't
    find an opaque ancestor (effectiveBg null), skip the contrast
    finding. Covers any remaining FP class the deeper fixes miss.

Verified end-to-end against an Opus iter-1 artifact on Tailwind v4 with
14 cream/cream FPs + 2 blue-link UA FPs before; 0 findings after, while
the color.html fixture's 12 real low-contrast cases continue to flag
(verified via direct detectHtml calls).

cli/engine/detect-antipatterns-browser.js is the generated browser
distribution — regenerated from .mjs via scripts/build-browser-detector.js
(no manual edits to the generated file).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* craft.md: tighten verbose passages, de-codex Step 6, cut redundancies

Cumulative reduction: 218 → 155 lines (-29%).

Step 0: drop the "Why this matters" paragraph at the end. The body of
Step 0 already makes the framework-pick point; the paragraph just
re-explains it with extra rhetoric.

Step 1: replace the 4-sentence "you must end your response" block with a
single line. The original said the same thing three different ways.

Step 3: trim the conditional / defensive scaffolding (Purpose subsection,
"do not skip because the eventual UI is semantic..." paragraph,
duplicated approval-loop guidance). Mock fidelity inventory preserved.

Step 4: drop the "keep UI text semantic" sentence; it duplicates Step 5's
"Semantic first" rule. The rasterized-vs-semantic decision rule stays.

Step 5: tighten each production-bar bullet to bold-lead + specifics
format. All 15 rules preserved (real content, mock ingredients, semantic
first, spacing/alignment, typography, state coverage, interaction quality,
icon set, build pipeline, image URL verification, optimized imagery,
premium motion, maintainability, technical cleanliness, ask-when-uncertain).

Step 6: rewrite around "look at what you built like a designer would —
your eyes are whatever the harness gives you." Drops Codex-specific
"In Codex, use browser-use" bias. Drops the verbose 3-step Read pattern
(condensed to one sentence). Drops the 1-8 numbered checklist (replaced
by a tight paragraph). Keeps the load-bearing rules: read the PNG,
don't fabricate iteration, mock fidelity reference, exit bar = studio
defensibility.

Step 7: drop the closing "Iterate based on feedback. Good design is
rarely right on the first pass" preachy filler.

All em-dashes converted to semicolons / colons / periods to satisfy
the skill prose validator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build: native subagent pipeline + Codex-only asset producer

Adds an agent cross-compile pipeline alongside the existing skill
pipeline. Sources live at skill/agents/*.md; providers that declare
agentFormat (codex-toml, claude-md) emit native subagent files. An
optional providers: <list> field on an agent gates which harnesses
get a copy; default (no field) ships everywhere.

The impeccable-asset-producer agent is opt-in to Codex only. It's
useful for Codex's native image generation path and is untested
elsewhere; Claude has no native image gen anyway.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* brand: inverse-test + cultural-symbol palette guardrail

Two additions to the brand register reference:

- Inverse slop test: describe the page the way a competitor would
  describe theirs. If that sentence fits the modal landing page in
  the category, restart.
- Palette guardrail: when a cultural-symbol palette is the obvious
  pull, reach past it. Let cultural reading come from typography,
  imagery, and copy.

Harness mirrors regenerated; some also catch up to the image-
verification paragraph from e3ad2ef that hadn't been re-synced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* PRODUCT.md: widen audience beyond developers

Designers, product managers, and engineers all use AI coding tools
and want better design output. Keeping the audience narrow to
"frontend and full-stack developers" understates who the skill is
actually for. Also retitles "developer" to "user/builder" in the
purpose statement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* site + build: bump rule count to 29, strip changelog from detector check

Two changes:
- site/pages/index.astro: three live mentions of "28 rules / checks"
  bumped to 29 after the body-text-viewport-edge rule landed in
  b9bf496.
- scripts/build.js: the detection-count validator was reading the
  unstripped content, so historical counts inside changelog entries
  (e.g. "28 rules" from an older release note) were flagging against
  the current detector total. The command-count check already strips
  the changelog ul; the detection check now does the same.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: align hero-eyebrow-chip fixture with relaxed rule gates

b9bf496 intentionally relaxed two gates in checkHeroEyebrow:
- removed the heading-size ≥ 48px anchor (modern hero h1s use
  clamp/vw/var that jsdom can't resolve)
- raised the eyebrow text ceiling from 30 to 60 chars

Two fixture cases that satisfied the negative side of the old gates
now match the rule:

- "Body-Sized Heading Below Eyebrow" — 24px h1 with tracked-caps
  label above. Per the rule's stated intent ("a tiny tan label
  directly above any h1 is the antipattern regardless of how big
  the h1 ends up"), this is a flag.
- "Long Uppercase Sentence Above Hero" — 46-char tracked-caps label
  is under the new 60-char ceiling, so still eyebrow-shaped.

Both cases moved from the should-pass column to should-flag, with
case descriptions rewritten to explain the gate they exercise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Paul Bakaus <paulbakaus@pauls-mbp-3.lan>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:11:18 -07:00
e587004ee4 Refactor: cleaner top-level directory structure (#138)
* refactor(content): merge content/site/ into site/content/

Phase 1 step 1 of the directory restructure. The dual content tree was
called out in CLAUDE.md as cleanup; both trees were already in sync
except for anti-patterns-catalog.js, which moves to site/data/.

- Delete content/site/skills/ and content/site/tutorials/ (duplicates of
  site/content/, which is what Astro's content collection actually reads).
- Move content/site/anti-patterns-catalog.js -> site/data/.
- Update scripts/lib/sub-pages-data.js and scripts/build.js to read from
  site/content/ and site/data/.
- Drop content/site/ from validateProse target list (site/content was
  already there).
- Rewrite the "Two content trees" section in CLAUDE.md as a single-tree
  pointer; update stale dev-server text mentioning the deleted
  server/index.js.

Tests: 186/186 pass. Skills build: clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(skill): rename source/skills/impeccable/ -> skill/

Phase 1 step 2 of the directory restructure. The path was redundantly
nested ("source/" wrapper plus "skills/impeccable/" — singular content
hidden behind the plural). Collapses to flat skill/SKILL.md +
skill/reference/ + skill/scripts/.

- Move source/skills/impeccable/ -> skill/.
- Rewrite scripts/lib/utils.js readSourceFiles(): drop the multi-skill
  iteration (CLAUDE.md commits to a single user-invocable skill); read
  skill/SKILL.md directly.
- Update scripts/build.js, scripts/generate-og-image.js, and the
  sub-pages data layer to point at skill/.
- Update tests/lib/utils.test.js: drop the "multi-skill" and "dir-name
  fallback" cases, update single-skill paths to skill/.
- Update tests/build.test.js similarly: drop "multiple skills"
  integration test, update paths.
- Update non-glob path joins in tests/framework-fixtures.test.mjs,
  tests/live-e2e/session.mjs, tests/live-e2e/agents/llm-agent.mjs,
  tools/live-loop.mjs.
- Update prose/text references in CLAUDE.md, AGENTS.md, DEVELOP.md,
  README.md, scripts/lib/sub-pages-data.js, bin/commands/skills.mjs,
  site/data/anti-patterns-catalog.js, site/pages/docs/[...slug].astro,
  docs/adr-live-variant-mode.md, docs/plans/.

Eval framework note: the separate impeccable-evals repo reads
../impeccable/source/skills/impeccable/ and needs a coordinated
rename to ../impeccable/skill/.

Tests: 186/186 pass. Skills build: clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: rename docs/ -> notes/

Phase 1 step 3 of the directory restructure. The internal docs/ dir
(ADRs and plans) clashed with the site's /docs route. Renaming it
"notes/" makes the difference unambiguous: notes/ is project-internal
process, /docs is the user-facing route under site/pages/docs/.

No code references the dir; the rename is a clean git mv.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(site): move public/ under site/public/

Phase 2 step 4 of the directory restructure. Public assets and the
Astro publicDir now live alongside the rest of the site, so site/
is fully self-contained for static content.

- git mv public site/public.
- astro.config.mjs: add publicDir: './site/public'. Astro defaults to
  ./public at the project root, so the override is required.
- scripts/build.js: write generated _data, _headers, _redirects,
  _routes.json, and js/detect-antipatterns-browser.js into
  site/public/. Also delete the dead _REMOVED() Bun static-site
  builder (replaced by Astro at #130; the placeholder no longer earns
  its keep).
- scripts/build.js validateProse: replace the stale public/index.html
  reference (deleted at the Astro migration) with site/pages/index.astro
  in the count-validation file list, restoring homepage drift detection.
- scripts/generate-og-image.js: write OG image into site/public/.
- scripts/screenshot-antipatterns.js: read examples from + write
  screenshots to site/public/antipattern-{examples,images}/.
- scripts/lib/sub-pages-data.js: load command demos from
  site/public/js/demos/commands.
- .gitignore: rename the public/* generator-output entries to
  site/public/*.
- CLAUDE.md: refresh CSS/data-file paths (still pointing at the old
  pre-Astro public/css/ + public/js/ tree), point the changelog and
  command-add checklists at site/pages/index.astro and
  site/scripts/data.js + site/scripts/components/framework-viz.js.

Cloudflare Pages note: functions/ stays at the repo root because
CF Pages auto-discovers it there with no configuration knob to
relocate. Moving it under site/ would either break deployment or
require a build-time copy step that adds more complexity than the
cleanup is worth.

Tests: 186/186 pass. Skills + site build clean. _headers,
_redirects, _routes.json, _data/ all land in build/ correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(cli): consolidate bin/ + src/ + lib/ under cli/

Phase 2 step 5 of the directory restructure. The CLI surface was split
across three top-level dirs whose names were easy to mistake for each
other (especially src/ vs source/ pre-step-2). Consolidates under cli/.

- git mv bin -> cli/bin (CLI entry + skills sub-command)
- git mv src -> cli/engine (detect-antipatterns engine + browser variant)
- git mv lib -> cli/lib (download-providers helper)

Update package.json:
- bin.impeccable: cli/bin/cli.js
- main + exports: cli/engine/detect-antipatterns.mjs and the
  ./browser variant
- files: ["cli/", "LICENSE"]

Update internal references:
- cli/bin/cli.js: dynamic import points at ../engine/, package.json
  read goes one level deeper (../../package.json).
- functions/api/download/[type]/[provider]/[id].js + bundle/[provider].js:
  cli/lib/download-providers.js path.
- scripts/build.js, scripts/build-browser-detector.js,
  scripts/build-extension.js: cli/engine path constants.
- scripts/lib/sub-pages-data.js, scripts/lib/utils.js, skill/scripts/
  live-server.mjs: comment refs.
- tests/detect-antipatterns{,-browser,-fixtures}.test.{js,mjs},
  tests/windows-path-fix.test.js: import + read paths.
- AGENTS.md, CLAUDE.md: doc paths.

Verified:
- npx node cli/bin/cli.js --version, --help, detect --help all work.
- bun run build, bun run build:browser, bun run build:extension all
  clean. Browser detector lands at cli/engine/detect-antipatterns-browser.js;
  extension/detector/detect.js still emits to the same location.
- bun run test: 186/186 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: update browser-detector paths missed in cli/ rename

Bugbot caught two runtime path leaks where the comment got renamed
to cli/engine/ but the actual code still used the old src/ segment.

- skill/scripts/live-server.mjs: detectPaths array now joins cli, engine,
  detect-antipatterns-browser.js for both the repo-relative lookup
  (4 dirs up from .claude/skills/impeccable/scripts/ to repo root) and
  the npm node_modules fallback. Without this fix, the detection
  overlay would silently not load during live-server sessions.

- scripts/build.js: the post-build copy of the browser detector into
  site/public/js/ was reading from src/. The if (fs.existsSync(...))
  guard meant the copy was silently skipping, so antipattern-examples
  pages would 404 on /js/detect-antipatterns-browser.js once the site
  was deployed.

Tests: 186/186 pass. Build clean. site/public/js/detect-antipatterns-browser.js
re-emits as expected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: cleanup-deprecated import path missed an extra .. in cli/ rename

Bugbot caught three call sites in cli/bin/commands/skills.mjs that
import '../../skill/scripts/cleanup-deprecated.mjs'. Pre-rename, that
was correct from bin/commands/ (one parent to bin/, one to repo root).
After moving the file from bin/commands/ to cli/bin/commands/, the
path is one directory deeper, so it needs three .. segments to reach
the repo root. Without the fix, every cleanup invocation throws on
import and gets swallowed by the surrounding try/catch — silent skip.

cli/bin/cli.js's package.json read already uses '../../package.json'
(the same depth pattern), confirming three levels is correct.

Verified: dynamic import resolves and exports the expected functions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: sweep stale path/file references missed in the restructure

Same root cause as the two bugbot finds: some references in moved or
related files weren't tracked because they didn't match a simple
sed pattern. Caught the rest by walking each moved dir's depth and
each Astro-migration deletion.

Stale path references (post-Astro migration, missed earlier):
- CLAUDE.md: legacy URL redirects "live in server/index.js" -> point
  at the actual sources (scripts/build.js generateCFConfig +
  site/public/_redirects).
- AGENTS.md: counts.js path (public/ -> site/public/), changelog file
  (public/index.html -> site/pages/index.astro), screenshots note
  (public/ -> site/), source-of-truth dirs (source/, src/ -> skill/,
  cli/).
- tests/detect-antipatterns-browser.test.mjs: comment about routes
  "in server/index.js".
- skill/reference/live.md: workflow.css example for "this repo" was
  pre-Astro (public/css/) -> site/styles/. (User-project Vite/Next
  example unchanged.)

Stale path that pointed at moved files:
- tests/skills-cli.test.js: CLI path was '..', 'bin', 'cli.js'; now
  '..', 'cli', 'bin', 'cli.js'. Test isn't wired into bun run test
  but it would have failed if invoked.

Dead files (orphaned by Astro migration, never cleaned up):
- tests/server/download-validation.test.js: imported from
  ../../server/lib/{validation,api-handlers}.js which were deleted in
  b8f09c8. Test was a silent failure waiting to happen.
- scripts/lib/render-markdown.js: 156-line module with zero consumers
  (the only caller, scripts/lib/render-page.js, was deleted in the
  Astro cleanup).
- scripts/build.js: dead commented-out generateSubPages import.

Tests: 186/186 pass. Build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(build): remove invalid Corepack packageManager spec

Cloudflare Pages rejects the build with `Unsupported package manager
specification (bun@1.3.11)`. The packageManager field follows
Corepack's syntax which only validates npm/pnpm/yarn — `bun@X.Y.Z`
parses as a malformed Corepack directive even though Bun itself
treats it as a hint.

Pre-existing on main since d874af0 (CF Pages deploy on main also
failing); just surfaces here because the PR triggers a fresh deploy.

CF Pages auto-detects Bun anyway (the build log confirms:
"Detected the following tools from environment: bun@1.3.11,
pnpm@10.11.1, nodejs@22.16.0"). Removing the field unblocks the
deploy without changing local dev behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Paul Bakaus <paulbakaus@pauls-mbp-3.lan>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 16:38:03 -07:00
d874af046a feat(live): make live sessions recoverable (#125)
* feat(live): make live sessions recoverable

tired of live mode losing the plot when the browser moved faster than the agent.
now the state is boring: journal it, resume it, finish it.

---
- add durable live-session journal, checkpoint events, and status/resume/complete commands
- split browser session storage into a testable helper and harden accept/discard completion
- fix Astro live CSS preview mode and add recovery/live E2E coverage
- declare Bun as the package manager and add a Bun-native audit script

* fix(live): acknowledge fallback recovery states

* fix(live): flush recoverable handoffs promptly

* fix(live): keep recovery handoffs accurate

* fix(live): preserve poll reply metadata

* fix(live): treat event HTTP failures as failed sends

* fix(live): acknowledge manual completion through helper

* Add .impeccable project state paths

* Fix live disconnect recovery phase

* Refine live CSS authoring contract

* Test live CSS authoring guidance

* Harden live LLM E2E recovery

* Fix live recovery review issues

---------

Co-authored-by: Paul Bakaus <paulbakaus@pauls-mbp-3.lan>
2026-05-03 19:03:22 -07:00
b8f09c8142 Migrate site from Bun to Astro (#130)
* feat(site): scaffold Astro migration, convert 3 pages

Phase 1+2 of the Astro migration:

- Astro v6.2.1 installed, srcDir: 'site', static output to build/
- Shared layout: Base.astro (head, fonts, meta, slots), Header.astro
  (star count in one place: 23k), Footer.astro
- CSS moved from public/css/ to site/styles/ (9 files, @import chains
  resolve via Vite)
- Three pages converted: privacy, cases/neo-mirai, live-mode
  (all return 200 on astro dev)

Remaining: designing, slop, homepage, content collections (docs),
JS migration, server/index.js deletion, build.js cleanup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(site): migrate all 6 main pages to Astro

Converts the remaining pages:
- designing/index.html → site/pages/designing/index.astro (551 lines)
- slop/index.html → site/pages/slop/index.astro (909 lines)
- index.html → site/pages/index.astro (1278 lines, the homepage)

Base.astro gains OG meta tag props, before-header/after-header
slots (for grain overlay and section nav), and configurable mainId.

Homepage uses link tags to public/css/ instead of frontmatter CSS
imports to avoid esbuild choking on :has() in main.css. Curly
braces inside <code> elements (CSS snippets in changelog) escaped
with HTML entities to prevent Astro JSX expression parsing.

All 6 pages return 200 on astro dev. Branch: feat/astro-migration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(site): content collections for docs and tutorials

Replaces the 1532-line build-sub-pages.js generator with Astro v6
content collections:

- 24 skill editorial files move to site/content/skills/
- 4 tutorial files move to site/content/tutorials/
- site/content.config.ts defines both collections with glob loaders
- site/pages/docs/[...slug].astro reads skills collection + command
  metadata from source/skills/ at build time
- site/pages/docs/index.astro renders the command grid grouped by
  category (create, evaluate, refine, simplify, harden, system)
- site/pages/tutorials/ mirrors the pattern with ordered index
- Doc.astro layout provides sidebar nav, breadcrumbs, and related-
  command chips from the COMMAND_RELATIONSHIPS data
- Category/relationship data extracted to site/data/sub-pages-data.ts

All 15 tested pages return 200: 6 main pages + 5 docs + 2 tutorials
+ 2 index pages. The old generator is not yet deleted (Task #6).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(site): move JS source from public/js/ to site/scripts/

Moves all 49 JS files (app.js + 48 in js/) into site/scripts/.
Vite now processes them through its module bundler instead of
serving them raw from public/.

app.js import paths updated from ./js/X to ./X (the js/ nesting
is gone since app.js now lives alongside the subdirectories).

Homepage and live-mode page switch from <script is:inline src="/app.js">
to Vite-processed <script> imports, so tree-shaking, bundling,
and minification happen automatically at build time.

public/js/ still exists for now (cleanup in Task #6) and the
generated/counts.js build output path needs updating there too.
@paper-design/shaders added to npm dependencies (was missing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(site): delete old Bun server, generator, and duplicated files

Cleanup after the Astro migration:

Deleted:
- server/index.js (233 lines, replaced by `astro dev`)
- scripts/build-sub-pages.js (1532 lines, replaced by content collections)
- scripts/lib/render-page.js (247 lines, replaced by Base.astro layout)
- content/site/partials/header.html (replaced by Header.astro component)
- public/index.html, privacy.html, designing/, live-mode/, cases/
  (replaced by .astro pages in site/pages/)
- public/css/ (moved to site/styles/)
- public/js/ old source files (moved to site/scripts/)
- public/app.js (moved to site/scripts/app.js)

Kept in public/:
- antipattern-examples/ (standalone HTML demos, not Astro pages)
- antipattern-images/, assets/, neo-mirai/ (static assets)
- js/detect-antipatterns-browser.js (referenced by antipattern examples)
- js/generated/counts.js (build output from scripts/build.js)
- _data/api/ (generated API data, now written to public/ so Astro
  passes it through to build/)

Updated:
- astro.config.mjs: added redirects (skills->docs, cheatsheet->docs,
  gallery->slop, neon-mirai->neo-mirai, etc.)
- package.json: dev->astro dev, build->build:skills+build:site,
  preview->astro preview
- scripts/build.js: removed buildStaticSite(), generateSubPages(),
  static-asset copying. API data writes to public/_data/ instead of
  build/_data/. Site-header validator is a no-op (shared component).
  Em-dash validator scans site/components + site/layouts, not pages
  (pages contain content from other sources like detector descriptions).
- .gitignore: removed public/slop/ entry

Tests: 186/186 pass. Skills build: clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(site): fix redirect config for Astro compatibility

Move the dynamic /skills/:id -> /docs/:id redirect to public/_redirects
(Cloudflare Pages native format) since Astro's redirect config can't
handle dynamic routes that don't match existing page patterns.

Remove duplicate trailing-slash redirect entries that caused warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(site): switch remaining pages from /css/ link tags to frontmatter imports

Doc.astro, docs/index, tutorials/index, and tutorials/[slug] were
still using <link href="/css/sub-pages.css"> which pointed at the
deleted public/css/ directory. Switched to frontmatter CSS imports
(import '../../styles/sub-pages.css') which Vite resolves from
site/styles/.

Homepage also switches from link tags to frontmatter imports for
main.css and sub-pages.css — the esbuild error that originally
forced the link-tag workaround was caused by unescaped curly braces
in the HTML content (since fixed), not by the CSS itself.

All pages verified visually in Chrome: homepage hero, foundation
grid, docs index (card grid with categories), docs detail (sidebar +
editorial content + visual mockups), designing (core loop diagram),
privacy, tutorials. Header renders with 23k stars on every page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(site): fix edge-to-edge sections, broken API paths, CSS links

Three fixes:

1. Homepage sections sat on the viewport edge because Base.astro's
   <main> lacked the site-content class (provides max-width + padding).
   Added mainClass prop to Base.astro; homepage sets mainClass="site-content".

2. "Failed to load commands" because app.js fetched /api/commands
   which only existed in the old Bun server's routing. Updated to
   fetch from /_data/api/commands.json (the static JSON files that
   build:skills writes to public/_data/).

3. CSS reference fix (previous commit was incomplete): Doc.astro,
   docs/index, tutorials pages all used <link href="/css/sub-pages.css">
   pointing at deleted public/css/. Switched to frontmatter imports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(site): add sidebar to docs index page

The docs index was using Base.astro directly without the skills-layout
grid, so it rendered without a sidebar. Added the same sidebar structure
from Doc.astro (category-grouped command list) and wrapped the content
in the skills-layout grid.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(site): extract footer CSS to shared file, import in Base.astro

Footer was unstyled on sub-pages because footer CSS lived only in
main.css (loaded by the homepage) not in sub-pages.css. Extracted
the 95 lines of footer rules into site/styles/footer.css and
imported it in Base.astro so every page gets footer styles regardless
of which page-specific CSS it loads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(demos): move landing-demo into repo, add as slop specimens

Moves ~/code/landing-demo/ into demos/landing-demo/ (without
node_modules or the redundant .claude/.agents skill copies — the
repo root's skill is found by walking up). PRODUCT.md, DESIGN.md,
DESIGN.json, PROMPT.md, and SCRIPT.md stay in place so running
Claude from demos/landing-demo/ picks up the project context.

Also copies both pages as slop specimens to public/antipattern-examples/
with the detector script baked in:
- new-slop-2026.html (Fraunces + warm cream editorial monoculture)
- old-slop-2022.html (purple gradient + glassmorphism + neon glow)

These can be linked from the slop page gallery alongside the
existing 11 synthetic specimens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(slop): replace single demo iframe with Then vs Now comparison

The "See it" section (01) on the slop page now shows two side-by-side
browser frames: 2022 slop (purple gradients, glassmorphism, neon glow)
and 2026 slop (Fraunces, warm cream, editorial restraint). Both run
the detector overlay live — hover either to see which rules fire.

Replaces the single visual-mode-demo.html iframe. Responsive: stacks
vertically on viewports below 900px.

Caption: "Same engine, different decade, both flagged."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(slop): switch to single-frame era toggle, center the section

Replaces the side-by-side dual-iframe layout with a single large
frame and a segmented 2022/2026 toggle. Clicking the toggle swaps
which iframe is visible (both pre-loaded, instant switch). Browser
chrome title updates to match the active era.

Centers the lede text and toggle above the frame for visual
cohesion with the full-width iframe below.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(slop): left-align See It section, toggle inline with lede

Moves the era toggle to the right of the lede paragraph using a
flex row (align-items: flex-end). Left-aligned text + right-docked
toggle matches the rest of the page's flow instead of standing out
as a centered island. Stacks vertically on narrow viewports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(slop): left-align iframe, remove max-width and auto margin

The visual-mode-preview had max-width: 1040px + margin: 0 auto
which centered it within the column. Override both in the
.slop-then-now context so the frame fills the full content width
flush with the text above. Caption left-aligned to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(site): update star count to 24k (24,062)

One file, one edit. The Astro migration working as intended.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(build): regenerate pnpm-lock.yaml for astro + shaders deps

Cloudflare Pages uses pnpm with frozen-lockfile. The lockfile was
stale after adding astro, @astrojs/cloudflare, and
@paper-design/shaders via npm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(build): resolve 3 bugbot review issues

1. Restore public/slop/ to .gitignore — prevents accidental legacy
   generator output from conflicting with the Astro page.

2. Move astro and @paper-design/shaders to devDependencies — these
   are site-build tools, not CLI runtime deps. Removes @astrojs/cloudflare
   entirely (unused; static output mode needs no adapter).

3. Fix Astro wiping build:skills output — CF config (_headers,
   _redirects, _routes.json) and API data now write to public/ so
   Astro copies them through. Dist ZIPs copy to build/_data/dist/
   as a post-build step (after Astro finishes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(build): merge duplicate devDependencies, use npx for astro CLI

The previous commit created a second devDependencies key in
package.json. JSON doesn't support duplicate keys — pnpm ignored
the first block (with astro), so `astro build` wasn't found.

Merged astro and @paper-design/shaders into the existing
devDependencies block. Changed `astro build/dev/preview` to
`npx astro build/dev/preview` so pnpm finds the local binary
on Cloudflare Pages (which doesn't add node_modules/.bin to PATH
by default).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(demos): remove private demo script and prompt from public repo

SCRIPT.md contained a detailed conference talk script with personal
delivery strategies, rehearsed Q&A answers, and venue details.
PROMPT.md contained the origin brief for the demo page. Neither
belongs in a public repo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(build): gitignore generated public/ artifacts, consolidate redirects

1. Generated files written to public/ by build:skills (API data,
   CF config, browser detector, counts.js) are now gitignored.
   Prevents noisy diffs and merge conflicts from committed build
   artifacts.

2. Removed duplicate redirects from astro.config.mjs. All redirects
   now live in one place: the _redirects file generated by
   scripts/build.js (which Cloudflare Pages processes natively).
   Eliminates the dual-maintenance risk where the two sources
   could drift apart.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 11:29:10 -07:00
Paul BakausandClaude Opus 4.7 5316809139 feat(site): merge /anti-patterns and /visual-mode into /slop
Top nav was at six items and growing. The anti-patterns catalog and the
visual-mode overlay demo were always two views of the same subject (the
rule set and seeing it caught in place), so collapsing them into one page
reduces nav weight and puts the catalog + live demo next to each other.

Four sections in one scroll: See it (iframe demo), Try it live (specimen
gallery), The catalog (full rule list), Run it yourself (invocation
methods). Sidebar TOC nests the catalog sections under "The catalog" for
deep linking. 301s from the old URLs preserve external links.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 13:13:56 -07:00
Paul BakausandClaude Opus 4.7 444f881295 fix(test): un-ignore fixture dist/ trees so they actually track
Top-level .gitignore excludes dist/ broadly, which silently dropped the
multipage-with-generator fixture's files/dist/*.html from the previous
commit. The fixture tests need those files on disk to copy into the
tmp repo and assert is-generated behavior — without them, the test
suite fails on a fresh clone.

Added a negation pattern that re-includes tests/framework-fixtures/**/dist/
paths. The real dist/ output directories elsewhere in the repo remain
ignored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:43:35 -07:00
Paul BakausandClaude Opus 4.7 81f880d030 feat(live): annotation capture, comment pins, drawing, and halftone loading shader
Adds a full annotation pipeline to /impeccable live. On Go, the browser
captures the selected element as a PNG (with annotations composed in),
uploads it to the live helper, and sends the generate event with the
screenshot path so the agent reads user intent visually instead of from
HTML alone.

Annotation tools (while an element is picked):
- Click inside the outline to drop a magenta comment pin with a text input
- Drag to paint a magenta SVG stroke (5 px click-vs-drag threshold)
- Click a pin to edit; double-click to delete; drag a pin to reposition
- Click a stroke to delete it (wider invisible hit path)
- Clear chip top-right wipes everything; hidden when no annotations

Capture pipeline:
- modern-screenshot vendored as an IIFE (scripts/modern-screenshot.umd.js)
  and lazy-loaded from the live helper
- Font fix: cross-origin @font-face rules are fetched and fonts are inlined
  as base64 data URIs before being handed to modern-screenshot via
  font.cssText, since SVGs rasterized via canvas can't fetch external
  resources (fix for "Impeccable" rendering bold-serif and items wrapping
  wrong in the capture)
- Annotations are temporarily attached to the live element (not only the
  clone) so computed styles resolve during the embed pass
- Session screenshots live in .impeccable-live/annotations/session-*/ in
  the project root (gitignored) so the agent's Read tool doesn't trip a
  per-path permission prompt

Loading shader (activates during GENERATING):
- WebGL overlay rendering the captured PNG as a halftone — cells with
  luma-driven dot radius, rendered on paper-cream underneath a magenta
  roller that sweeps top-to-bottom with a 3.4s cycle and clean overshoot
- Fixed asymmetric bandAt() using one-sided smoothsteps (previous reversed
  smoothstep was undefined on d>0, giving "trail=1 everywhere below")
- Graceful <img> fallback when WebGL is unavailable; prefers-reduced-motion
  freezes the band at t=0

Server:
- POST /annotation endpoint (raw image/png body, token + eventId query),
  session-scoped tmpdir cleaned up on shutdown
- GET /modern-screenshot.js serves the vendored UMD with aggressive caching
- Optional screenshotPath / comments / strokes fields on generate events
- Fixed pre-existing /source crash on ENOENT (writeHead called twice)

Agent side:
- reference/live.md step 0 tells the agent to Read the screenshot first,
  with four rules for interpreting annotations: comments are position-
  anchored and scoped to the sub-element under their {x,y}; strokes are
  gestures (loop=focus, arrow=direction, cross=delete); comments and
  strokes are independent unless adjacent; don't silently guess on
  ambiguous strokes

Also:
- Generating bar no longer claims "Generating 1 of 3..." (variants arrive
  atomically) — now says "Generating N variants..."
- tests/live-server.test.mjs fixed to read the PID file from project root,
  matching the server; adds coverage for the new endpoints and validator
  fields
- .impeccable-live/ added to .gitignore

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 09:38:48 -07:00
Paul Bakaus 66630a0ead chore: sync skills, live tooling, and eval ignores
- Ship design-parser and refreshed live scripts/document refs across provider
  skill trees; align live.mjs and live-poll with source
- Update build/skills harness metadata, skills CLI test, and devDependencies
  (AI SDKs, zod)
- Gitignore .codex/ harness artifacts and tests/evals-v2/

Made-with: Cursor
2026-04-17 18:04:45 -07:00
Paul BakausandClaude Opus 4.6 866973fdcf Track PRODUCT.md + DESIGN.md as reference implementations
This repo is the canonical example of impeccable — users who install the
skill should be able to look at how a real project uses PRODUCT.md and
(later) DESIGN.md. Ungitignored both files.

Also rewrote PRODUCT.md itself to match the new schema from teach.md
(# Product → Users / Product Purpose / Brand Personality / Anti-references
/ Design Principles / Accessibility), dropping the legacy "Design Context"
wrapper and the Aesthetic Direction section that belongs in DESIGN.md.

.impeccable.md stays gitignored as legacy (the loader auto-migrates it).

DESIGN.md for this project is pending — to be generated with
/impeccable document once we run it on our own codebase.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:15:36 -07:00
Paul BakausandClaude Opus 4.6 af2d6e1194 Support PRODUCT.md + DESIGN.md as canonical context files
Pioneers a two-file convention for project context:
- PRODUCT.md (strategic): users, brand, principles — answers who/what/why
- DESIGN.md (visual): follows Google's Stitch DESIGN.md spec — answers how-it-looks

Both files live at the repo root. Filename matching is case-insensitive.
DESIGN.md wins on visual conflicts, PRODUCT.md wins on strategic/voice.

Legacy .impeccable.md is auto-migrated to PRODUCT.md on first read by the
new shared loader. This is silent and one-shot — the rename is permanent.

What changed:
- New scripts/load-context.mjs: shared context loader used by every command
  that needs project context. Reads both files, handles legacy migration.
- New reference/document.md: /impeccable document command that generates
  DESIGN.md by auto-extracting tokens (colors, typography, spacing, radii,
  shadows, components) from CSS/Tailwind/theme files, then asking the user
  to confirm descriptive language for atmosphere and color character.
  Follows Google's Stitch DESIGN.md format for tool compatibility.
- SKILL.md Context Gathering Protocol updated to load both files and
  nudge the user to run /impeccable document when DESIGN.md is missing.
- reference/teach.md rewritten to split discovery cleanly: strategic
  questions go to PRODUCT.md, visual/design-system work is delegated to
  /impeccable document (skipped on empty projects).
- reference/live.md consumes {product, design, productPath, designPath,
  migrated} from the loader instead of a single context blob.
- scripts/live.mjs uses the shared loader instead of inline file reading.
- Command count updated 22 → 23 (new: document). Metadata, router table,
  command menu, periodic table viz, and homepage data all updated.
- .gitignore adds PRODUCT.md + DESIGN.md (repo-local, not shared).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:14:13 -07:00
Paul BakausandClaude Opus 4.6 8030bc226a Add live-inject.mjs: per-project config for instant script tag management
First live run: agent auto-detects framework and writes a small config.json
(file, insertBefore/insertAfter anchor, comment syntax). Every subsequent
run: live-inject.mjs handles insert/remove deterministically, no LLM needed.

The config lives at {scripts_path}/config.json and is gitignored — it's a
per-project cache that wipes on skill update and regenerates on next use.

- New live-inject.mjs: --port (insert), --remove, --check modes
- Idempotent insert: re-running with a different port replaces cleanly
- Reference doc: one-time detection step, then instant insert/remove

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:44:26 -07:00
Paul BakausandClaude Opus 4.6 4092ee5f22 Move PID file to project root (.impeccable-live.json)
os.tmpdir() returns /var/folders/.../T/ on macOS, not /tmp/. The skill
reference was telling the agent to cat /tmp/impeccable-live.json which
didn't exist. Moving the PID file to the project root makes it
predictable across platforms and project-scoped (multiple projects can
run independent live sessions).

Changed in: live-server.mjs, live-poll.mjs, live.md reference.
Added .impeccable-live.json to .gitignore.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:32:42 -07:00
Paul BakausandClaude Opus 4.6 9341feeac1 Ignore package-lock.json (project uses bun.lock)
npm subprocesses can regenerate a stray package-lock.json (last time
this happened, it was reverted in 3ca60a8). Add it to .gitignore so it
stops showing up as untracked.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:26:45 -07:00
Paul BakausandClaude Opus 4.6 b0f44f83c6 Consolidate 18 skills into 1 /impeccable skill with 20 commands
Biggest change in a while. Users previously had 18 standalone skill
entries cluttering their /menu; now they have one entry (/impeccable)
that routes to 20 specialized commands via argument dispatch. The pin
mechanism (/impeccable pin audit) restores standalone shortcuts on
demand for commands users hit all the time.

## Architecture

- Single /impeccable skill with command router section in SKILL.md
- 20 commands served via reference files under source/skills/impeccable/reference/
- /impeccable pin <command> creates a lightweight redirect shim so users
  who prefer /audit, /polish, etc. can still have them
- Context gathering (teach) auto-runs on first use
- command-metadata.json is the single source of truth for command
  descriptions, argument hints, and relationships

## Site rewrite

- Docs URL: /skills renamed to /docs (with /skills permanent redirects)
- Homepage hero frames Impeccable as "one skill with 20 commands"
- "Get Started" split into 50/50 install + how-to-use with editorial
  numbered steps, /impeccable shown as the home command with three modes
- New /docs overview: home command hero card + dense category rows
  matching the old cheatsheet density, with leads-to/pairs-with/
  combines-with relationship metadata served from a shared source
- Cheatsheet merged into /docs, /cheatsheet redirects
- Magazine spread and mobile cards show /impeccable as a stacked
  namespace label above the command name at full display size
- Periodic table updated with craft/teach/extract as first-class cells
- Skill detail pages generate from reference files, with an editorial
  wrapper per command for tagline + body
- Tutorials and anti-patterns pages updated to use /impeccable <cmd>

## Build system

- Dead code removed (scripts/lib/transformers/shared.js)
- Build log wording fixed ("1 skill" not "1 skills (1 user-invocable)")
- generateApiData fallback branch removed (throws loudly if metadata
  missing instead of silently degrading)
- Commands API includes editorial tagline alongside the long description;
  UI surfaces prefer tagline for human display, description for auto-
  trigger keyword matching

## Gitignore

- Added .claude/scheduled_tasks.lock, .claude/settings.local.json to
  ignore list (local Claude Code state that should not be tracked).
- Harness skill directories (.claude/skills/, .agents/skills/, etc.)
  remain tracked by design: npx skills reads them from this repo at
  install time and they enable clean submodule use.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:45:17 -07:00
Paul Bakaus 27d1b13bc2 New /visual-mode top-level page, pull gallery out of /anti-patterns
The 'In the wild' section at the bottom of /anti-patterns was
mischaracterizing synthetic fixtures as real examples and was buried
deep in a taxonomy of detection rules. The specimens belong somewhere
that frames them as what they actually are: live pages you can click
into to experience Visual Mode. Split them off into a new top-level
page that also finally gives Visual Mode first-class treatment.

- New /visual-mode page, top-level nav item, single-column layout (no
  sidebar). Structure:
    1. Editorial header with an "Live detection overlay" eyebrow.
    2. Live iframe embed of visual-mode-demo.html inside mac-window
       chrome, same preview component the homepage uses.
    3. "Three ways to run it" section with three method cards:
         - /critique runs the overlay inside its browser pass
         - `npx impeccable live` starts a standalone overlay server
         - Chrome extension, marked coming soon, with a cream bg
    4. "Try it live" gallery of the 11 synthetic specimens as
       clickable cards. Each links to /antipattern-examples/{id}.html
       where the detector script is already injected so the reader
       lands on a live overlay.
- scripts/build-sub-pages.js: new renderVisualModeMain(); visualMode
  added to outDirs; generator loop writes /visual-mode/index.html.
- server/index.js: new /visual-mode route serving the generated file.
- Top nav on every page gains 'Visual Mode' between Anti-Patterns
  and the GitHub pill. Updated the partial + all 4 hand-authored
  HTML pages.
- .gitignore adds public/visual-mode/.

- /anti-patterns: 'In the wild' section and its TOC entry removed.
  Replaced with a one-line pointer at the end of the lede: "Want to
  see them live on real pages? Try Visual Mode." GALLERY_ITEMS stays
  in the catalog file (now used by /visual-mode only).

- public/css/sub-pages.css: new .visual-mode-page-body + .visual-mode-*
  classes. Ports the mac-window chrome (dots + mono title) from
  main.css, adds three-card method grid, and reuses the existing
  .gallery-card styles for the specimen list.

Clean up a few em-dashes in the catalog (block comments + one visible
visual example) so the build-time validator stays clean.

Server restart required to pick up the new /visual-mode route.
2026-04-08 13:04:23 -07:00
Paul Bakaus 7847daffff Generate /skills index + 21 auto-rendered skill detail pages
Ships the first new sub-page section. Every user-invocable skill now has
its own page at /skills/{id}, with the canonical SKILL.md body rendered
via marked. The index at /skills lists all 21 skills grouped by category.

Editorial wrappers are opt-in: if content/site/skills/{id}.md exists, it
renders above the canonical body (with a "The skill itself" divider).
All 21 pages currently ship with the auto-rendered body only; hand-written
wrappers land in the next few commits.

- scripts/lib/sub-pages-data.js: builds the data model. Reuses
  readSourceFiles() from lib/utils.js for skill content; parses the
  ANTIPATTERNS array out of src/detect-antipatterns.mjs; reads optional
  editorial wrappers from content/site/skills/*.md; validates that every
  user-invocable skill has a category entry (build fails loudly if not).
- scripts/build-sub-pages.js: orchestrator. Writes generated HTML into
  public/skills/*.html (gitignored). Called from both scripts/build.js
  (before buildStaticSite) and server/index.js (at module load) so dev
  and prod share the same generation code path.
- scripts/lib/render-page.js: new assetDepth parameter so generated
  pages one level deep under public/ use relative paths (../favicon.svg,
  ../css/sub-pages.css) that Bun's HTML loader can resolve on disk.
- scripts/build.js: pass generated files into Bun.build entrypoints;
  post-process to flatten build/public/* → build/* (Bun preserves the
  public/ prefix when entrypoints span multiple depths).
- server/index.js: generateSubPages() runs at module load; new routes
  /skills, /skills/:id, /anti-patterns, /tutorials, /tutorials/:slug
  serve the pre-generated files via Bun.file().
- public/css/sub-pages.css: adds sub-page layout shell, skills index
  grouped-list styling, skill detail header/meta chips/divider, collapsed
  <details> reference sections, and a .prose block for rendered markdown
  with editorial typography, code blocks, and inline code.

Verified: bun run build produces 26 HTML files (4 hand-authored + 22
generated), all flat under build/. Dev server returns 200 on /skills,
/skills/polish, /skills/impeccable, /skills/critique. Tests pass.
2026-04-08 09:16:19 -07:00
Paul Bakaus d85efab51f Refine homepage layout: install, Stay updated, Changelog/FAQ, Visual Mode
- Restructure install section into a split primary card with the
  recommended path on the left and other install methods on the right
- Drop the prefix toggle UI; bundle name is now baked into data-bundle
- Balance the Stay updated step as paired Substack iframe + X follow card
  with matching height, border, and treatment
- Pair Changelog and FAQ side-by-side on desktop with a centered divider
  and shared border-top; section nav highlights both when in view
- Tone down the Visual Mode demo by removing side-tab borders, stacked
  card icons, and tiny body text so the overlay reads cleanly
- Refine the Chrome extension callout: drop the pill badge, use an
  editorial eyebrow above the title, enlarge the thumbnail
- Exclude private evals/ directory from version control
2026-04-07 18:37:08 -07:00
Paul BakausandClaude Opus 4.6 e961d56252 Add Chrome DevTools extension for anti-pattern detection
Adds a Manifest V3 Chrome extension that injects the detector when
DevTools opens, with a dedicated panel for browsing findings, a toolbar
popup for quick scan/toggle, and per-rule settings synced via
chrome.storage. Categorizes anti-patterns into AI slop vs quality
issues with visual differentiation (sparkle prefix, panel grouping).
Overlay labels are polished with flush positioning, cycling for
multi-finding elements, and synchronized hover darkening.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:18:36 -07:00
Paul BakausandClaude Opus 4.6 3974789903 Remove detection engine files, now in @impeccable/detect package
Remove all detection engine source, tests, fixtures, and build scripts
that have been extracted to the @impeccable/detect package (BSL-1.1).

- Delete source/skills/critique/scripts/ (detect-antipatterns.mjs, browser.js)
- Delete scripts/build-browser-detector.js
- Delete tests/detect-antipatterns*.test.* and tests/fixtures/antipatterns/
- Delete .claude/skills/critique/scripts/detect-antipatterns-browser.js
- Update scripts/build.js to read detection count from npm package
- Update server/index.js to serve browser script from npm package
- Update CLAUDE.md to reference @impeccable/detect
- Update package.json test script (detection tests removed)
- Update .gitignore (remove obsolete browser script entry)
- Rebuild all provider skill distributions with updated critique skill

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 16:32:57 -07:00
Paul BakausandClaude Opus 4.6 8d0e9de26d Prepare CLI for npm: v2.0.1, Node compat, npm-specific README
- Rename bin/impeccable.mjs to bin/impeccable (npm rejects .mjs in bin)
- Shebang: #!/usr/bin/env node (works without Bun)
- Add README.npm.md with CLI-focused docs, swapped in during publish
- Build browser script to source/ dir so URL scanning works in npm pkg
- Include browser script in files field
- Move website-only deps (archiver, motion, playwright) to devDependencies
- jsdom as dependency, puppeteer as optionalDependency
- Bump version to 2.0.1 across package.json, plugin.json, marketplace.json

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 12:17:59 -07:00
Paul BakausandClaude Opus 4.6 4305dfd362 Add .impeccable.md to .gitignore
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 23:33:46 -07:00
Paul BakausandClaude Opus 4.6 5b5bba6488 Add .wrangler/ to gitignore
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 11:29:07 -07:00
Paul BakausandClaude Opus 4.5 52d99fb31c Remove Tailwind and simplify CSS build process
- Remove Tailwind CSS and related dependencies (tailwindcss, @tailwindcss/cli, bun-plugin-tailwind)
- Remove unused dependencies (lenis, @paper-design/shaders, three)
- Convert @theme variables to CSS custom properties in :root
- Add minimal CSS reset for browser consistency
- Update server to serve /css/* and /js/* routes directly
- Fix conflicting CSS rules that broke commands section layout
- Delete bunfig.toml (no longer needed without Tailwind plugin)

The site now uses native CSS imports via Bun's built-in bundler,
eliminating the need for separate Tailwind compilation step.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 14:58:39 -08:00
Paul BakausandClaude Opus 4.5 79d06b61ab Add Claude Code plugin marketplace support
- Create .claude-plugin/marketplace.json and plugin.json for marketplace install
- Add install command UI to website download card
- Update .gitignore to ignore entire dist/ folder (generated files)
- Remove tracked dist/ files from repository

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 14:24:53 -08:00
Paul BakausandClaude Opus 4.5 1492717504 Fix: Use Tailwind CLI for CSS compilation before Bun bundler
Bun's native CSS bundler doesn't support Tailwind v4's @theme directive.
Solution:
1. Run Tailwind CLI first to compile main.css → styles.css
2. Reference styles.css in HTML
3. Bun HTML bundler then bundles everything together

Build output:
- JS: 82 KB (minified, bundled)
- CSS: 63 KB (minified, includes all custom styles)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 14:42:27 -08:00
Paul BakausandClaude Opus 4.5 4cb8055040 Refactor to use Bun's HTML static site bundler
- Use HTML as entrypoint for Bun.build() instead of separate JS/CSS builds
- Leverage bun-plugin-tailwind for automatic Tailwind CSS processing
- Output bundled site to ./build/ directory
- Update vercel.json outputDirectory to build
- Clean up obsolete build artifacts and gitignore entries

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 14:37:00 -08:00
Paul BakausandClaude Opus 4.5 2fb61defca Add Bun bundler for frontend JavaScript
- Bundle all frontend JS with Bun.build() (82KB minified)
- Dependencies (motion, lenis, three) are now bundled instead of CDN
- Remove import map from index.html (no longer needed)
- Output to public/dist/app.bundle.js with source map
- Add public/dist/ to .gitignore (generated during build)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 14:33:03 -08:00
Paul BakausandClaude Opus 4.5 7aefe19b2a Fix Vercel deployment: Tailwind CSS, API endpoints, and Bun runtime
- Add missing /api/patterns.js endpoint (was causing 404)
- Fix Tailwind CSS: compile with @tailwindcss/cli instead of Bun virtual module
- Update API handlers to use standard Vercel function export format
- Configure vercel.json with proper Bun runtime (runtime: "bun@1")
- Add @tailwindcss/cli to devDependencies
- Add generated styles.css to .gitignore

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 14:21:43 -08:00
Paul Bakaus 89d179143b stubs and website 2025-11-16 18:52:23 -08:00
Paul Bakaus 661293796c initial commit and build out 2025-11-16 14:54:35 -08:00
Paul BakausandGitHub 9ee3afcdf9 Initial commit 2025-11-15 17:52:04 -08:00