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>
This commit is contained in:
Paul Bakaus
2026-05-04 16:38:03 -07:00
committed by GitHub
co-authored by Paul Bakaus Claude Opus 4.7
parent 2aeac48b19
commit e587004ee4
256 changed files with 383 additions and 2650 deletions
+1 -1
View File
@@ -411,7 +411,7 @@ When `_acceptResult.carbonize === true`, the accepted variant was stitched into
Do these five steps in the current thread, synchronously, before the next poll. Do not poll again until the file is clean.
1. **Locate the carbonize block** in the source file (`_acceptResult.file`). It's bracketed by `<!-- impeccable-carbonize-start SESSION_ID -->` and `<!-- impeccable-carbonize-end SESSION_ID -->` and contains a `<style data-impeccable-css="SESSION_ID">` element. If the variant declared parameters, an `<!-- impeccable-param-values SESSION_ID: {...} -->` comment sits alongside the style tag with the user's chosen values; read it first; it drives steps 3 and 4 below.
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `public/css/workflow.css` for this repo, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `site/styles/workflow.css` for an Astro project, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
3. **Bake in parameter values while rewriting selectors.** For `@scope ([data-impeccable-variant="N"])` wrappers: retarget to real, semantic classes on the accepted HTML (`.why-visual--v2 .v2-label { … }`). For `:scope[data-p-<id>="VALUE"]` selectors: keep only the branch matching the chosen value from the param-values comment; drop the others (they're dead after accept). For `var(--p-<id>, DEFAULT)` in the CSS: either substitute the literal value, or if the param is still useful as a knob going forward, leave the var and update its initial declaration to the chosen value.
4. **Unwrap the accepted content.** Delete the `<div data-impeccable-variant="N" style="display: contents">` that wraps it. Drop `data-impeccable-params` and any `data-p-*` attributes from it; those are live-mode plumbing, not source.
5. **Delete the inline `<style>` block, the `<!-- impeccable-param-values -->` comment if present, and both `<!-- impeccable-carbonize-start/end -->` markers.** Also drop any `@scope` rules for variants other than the accepted one; those are dead code now.
@@ -156,11 +156,11 @@ function broadcast(msg) {
function loadBrowserScripts() {
// Detection script: look relative to the skill scripts dir, then fall back
// to the npm package location (src/detect-antipatterns-browser.js).
// to the npm package location (cli/engine/detect-antipatterns-browser.js).
// This one IS cached — detect.js rarely changes during a session.
const detectPaths = [
path.join(__dirname, '..', '..', '..', '..', 'src', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'src', 'detect-antipatterns-browser.js'),
path.join(__dirname, '..', '..', '..', '..', 'cli', 'engine', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'cli', 'engine', 'detect-antipatterns-browser.js'),
];
let detectScript = '';
for (const p of detectPaths) {
+1 -1
View File
@@ -411,7 +411,7 @@ When `_acceptResult.carbonize === true`, the accepted variant was stitched into
Do these five steps in the current thread, synchronously, before the next poll. Do not poll again until the file is clean.
1. **Locate the carbonize block** in the source file (`_acceptResult.file`). It's bracketed by `<!-- impeccable-carbonize-start SESSION_ID -->` and `<!-- impeccable-carbonize-end SESSION_ID -->` and contains a `<style data-impeccable-css="SESSION_ID">` element. If the variant declared parameters, an `<!-- impeccable-param-values SESSION_ID: {...} -->` comment sits alongside the style tag with the user's chosen values; read it first; it drives steps 3 and 4 below.
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `public/css/workflow.css` for this repo, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `site/styles/workflow.css` for an Astro project, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
3. **Bake in parameter values while rewriting selectors.** For `@scope ([data-impeccable-variant="N"])` wrappers: retarget to real, semantic classes on the accepted HTML (`.why-visual--v2 .v2-label { … }`). For `:scope[data-p-<id>="VALUE"]` selectors: keep only the branch matching the chosen value from the param-values comment; drop the others (they're dead after accept). For `var(--p-<id>, DEFAULT)` in the CSS: either substitute the literal value, or if the param is still useful as a knob going forward, leave the var and update its initial declaration to the chosen value.
4. **Unwrap the accepted content.** Delete the `<div data-impeccable-variant="N" style="display: contents">` that wraps it. Drop `data-impeccable-params` and any `data-p-*` attributes from it; those are live-mode plumbing, not source.
5. **Delete the inline `<style>` block, the `<!-- impeccable-param-values -->` comment if present, and both `<!-- impeccable-carbonize-start/end -->` markers.** Also drop any `@scope` rules for variants other than the accepted one; those are dead code now.
@@ -156,11 +156,11 @@ function broadcast(msg) {
function loadBrowserScripts() {
// Detection script: look relative to the skill scripts dir, then fall back
// to the npm package location (src/detect-antipatterns-browser.js).
// to the npm package location (cli/engine/detect-antipatterns-browser.js).
// This one IS cached — detect.js rarely changes during a session.
const detectPaths = [
path.join(__dirname, '..', '..', '..', '..', 'src', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'src', 'detect-antipatterns-browser.js'),
path.join(__dirname, '..', '..', '..', '..', 'cli', 'engine', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'cli', 'engine', 'detect-antipatterns-browser.js'),
];
let detectScript = '';
for (const p of detectPaths) {
+1 -1
View File
@@ -411,7 +411,7 @@ When `_acceptResult.carbonize === true`, the accepted variant was stitched into
Do these five steps in the current thread, synchronously, before the next poll. Do not poll again until the file is clean.
1. **Locate the carbonize block** in the source file (`_acceptResult.file`). It's bracketed by `<!-- impeccable-carbonize-start SESSION_ID -->` and `<!-- impeccable-carbonize-end SESSION_ID -->` and contains a `<style data-impeccable-css="SESSION_ID">` element. If the variant declared parameters, an `<!-- impeccable-param-values SESSION_ID: {...} -->` comment sits alongside the style tag with the user's chosen values; read it first; it drives steps 3 and 4 below.
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `public/css/workflow.css` for this repo, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `site/styles/workflow.css` for an Astro project, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
3. **Bake in parameter values while rewriting selectors.** For `@scope ([data-impeccable-variant="N"])` wrappers: retarget to real, semantic classes on the accepted HTML (`.why-visual--v2 .v2-label { … }`). For `:scope[data-p-<id>="VALUE"]` selectors: keep only the branch matching the chosen value from the param-values comment; drop the others (they're dead after accept). For `var(--p-<id>, DEFAULT)` in the CSS: either substitute the literal value, or if the param is still useful as a knob going forward, leave the var and update its initial declaration to the chosen value.
4. **Unwrap the accepted content.** Delete the `<div data-impeccable-variant="N" style="display: contents">` that wraps it. Drop `data-impeccable-params` and any `data-p-*` attributes from it; those are live-mode plumbing, not source.
5. **Delete the inline `<style>` block, the `<!-- impeccable-param-values -->` comment if present, and both `<!-- impeccable-carbonize-start/end -->` markers.** Also drop any `@scope` rules for variants other than the accepted one; those are dead code now.
@@ -156,11 +156,11 @@ function broadcast(msg) {
function loadBrowserScripts() {
// Detection script: look relative to the skill scripts dir, then fall back
// to the npm package location (src/detect-antipatterns-browser.js).
// to the npm package location (cli/engine/detect-antipatterns-browser.js).
// This one IS cached — detect.js rarely changes during a session.
const detectPaths = [
path.join(__dirname, '..', '..', '..', '..', 'src', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'src', 'detect-antipatterns-browser.js'),
path.join(__dirname, '..', '..', '..', '..', 'cli', 'engine', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'cli', 'engine', 'detect-antipatterns-browser.js'),
];
let detectScript = '';
for (const p of detectPaths) {
+1 -1
View File
@@ -411,7 +411,7 @@ When `_acceptResult.carbonize === true`, the accepted variant was stitched into
Do these five steps in the current thread, synchronously, before the next poll. Do not poll again until the file is clean.
1. **Locate the carbonize block** in the source file (`_acceptResult.file`). It's bracketed by `<!-- impeccable-carbonize-start SESSION_ID -->` and `<!-- impeccable-carbonize-end SESSION_ID -->` and contains a `<style data-impeccable-css="SESSION_ID">` element. If the variant declared parameters, an `<!-- impeccable-param-values SESSION_ID: {...} -->` comment sits alongside the style tag with the user's chosen values; read it first; it drives steps 3 and 4 below.
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `public/css/workflow.css` for this repo, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `site/styles/workflow.css` for an Astro project, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
3. **Bake in parameter values while rewriting selectors.** For `@scope ([data-impeccable-variant="N"])` wrappers: retarget to real, semantic classes on the accepted HTML (`.why-visual--v2 .v2-label { … }`). For `:scope[data-p-<id>="VALUE"]` selectors: keep only the branch matching the chosen value from the param-values comment; drop the others (they're dead after accept). For `var(--p-<id>, DEFAULT)` in the CSS: either substitute the literal value, or if the param is still useful as a knob going forward, leave the var and update its initial declaration to the chosen value.
4. **Unwrap the accepted content.** Delete the `<div data-impeccable-variant="N" style="display: contents">` that wraps it. Drop `data-impeccable-params` and any `data-p-*` attributes from it; those are live-mode plumbing, not source.
5. **Delete the inline `<style>` block, the `<!-- impeccable-param-values -->` comment if present, and both `<!-- impeccable-carbonize-start/end -->` markers.** Also drop any `@scope` rules for variants other than the accepted one; those are dead code now.
@@ -156,11 +156,11 @@ function broadcast(msg) {
function loadBrowserScripts() {
// Detection script: look relative to the skill scripts dir, then fall back
// to the npm package location (src/detect-antipatterns-browser.js).
// to the npm package location (cli/engine/detect-antipatterns-browser.js).
// This one IS cached — detect.js rarely changes during a session.
const detectPaths = [
path.join(__dirname, '..', '..', '..', '..', 'src', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'src', 'detect-antipatterns-browser.js'),
path.join(__dirname, '..', '..', '..', '..', 'cli', 'engine', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'cli', 'engine', 'detect-antipatterns-browser.js'),
];
let detectScript = '';
for (const p of detectPaths) {
+1 -1
View File
@@ -411,7 +411,7 @@ When `_acceptResult.carbonize === true`, the accepted variant was stitched into
Do these five steps in the current thread, synchronously, before the next poll. Do not poll again until the file is clean.
1. **Locate the carbonize block** in the source file (`_acceptResult.file`). It's bracketed by `<!-- impeccable-carbonize-start SESSION_ID -->` and `<!-- impeccable-carbonize-end SESSION_ID -->` and contains a `<style data-impeccable-css="SESSION_ID">` element. If the variant declared parameters, an `<!-- impeccable-param-values SESSION_ID: {...} -->` comment sits alongside the style tag with the user's chosen values; read it first; it drives steps 3 and 4 below.
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `public/css/workflow.css` for this repo, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `site/styles/workflow.css` for an Astro project, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
3. **Bake in parameter values while rewriting selectors.** For `@scope ([data-impeccable-variant="N"])` wrappers: retarget to real, semantic classes on the accepted HTML (`.why-visual--v2 .v2-label { … }`). For `:scope[data-p-<id>="VALUE"]` selectors: keep only the branch matching the chosen value from the param-values comment; drop the others (they're dead after accept). For `var(--p-<id>, DEFAULT)` in the CSS: either substitute the literal value, or if the param is still useful as a knob going forward, leave the var and update its initial declaration to the chosen value.
4. **Unwrap the accepted content.** Delete the `<div data-impeccable-variant="N" style="display: contents">` that wraps it. Drop `data-impeccable-params` and any `data-p-*` attributes from it; those are live-mode plumbing, not source.
5. **Delete the inline `<style>` block, the `<!-- impeccable-param-values -->` comment if present, and both `<!-- impeccable-carbonize-start/end -->` markers.** Also drop any `@scope` rules for variants other than the accepted one; those are dead code now.
@@ -156,11 +156,11 @@ function broadcast(msg) {
function loadBrowserScripts() {
// Detection script: look relative to the skill scripts dir, then fall back
// to the npm package location (src/detect-antipatterns-browser.js).
// to the npm package location (cli/engine/detect-antipatterns-browser.js).
// This one IS cached — detect.js rarely changes during a session.
const detectPaths = [
path.join(__dirname, '..', '..', '..', '..', 'src', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'src', 'detect-antipatterns-browser.js'),
path.join(__dirname, '..', '..', '..', '..', 'cli', 'engine', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'cli', 'engine', 'detect-antipatterns-browser.js'),
];
let detectScript = '';
for (const p of detectPaths) {
+13 -13
View File
@@ -67,24 +67,24 @@ evals/
tests/evals-v2/
# Generated sub-pages (legacy, now replaced by Astro content collections)
public/docs/
public/anti-patterns/
public/tutorials/
public/visual-mode/
public/slop/
site/public/docs/
site/public/anti-patterns/
site/public/tutorials/
site/public/visual-mode/
site/public/slop/
# Build artifacts written to public/ so Astro copies them to build/
public/_data/
public/_headers
public/_redirects
public/_routes.json
public/js/detect-antipatterns-browser.js
public/js/generated/
# Build artifacts written to site/public/ so Astro copies them to build/
site/public/_data/
site/public/_headers
site/public/_redirects
site/public/_routes.json
site/public/js/detect-antipatterns-browser.js
site/public/js/generated/
# Note: harness skill directories (.claude/skills/, .cursor/skills/, etc.)
# are intentionally tracked. npx skills reads them from this repo at install
# time, and they enable clean submodule use. Run `bun run build` to refresh
# them after editing source/skills/.
# them after editing skill/.
#
# Codex CLI consumes `.agents/skills/`; `.codex/` is not used. Ignore it so
# local artifacts or old trees are never committed.
+1 -1
View File
@@ -411,7 +411,7 @@ When `_acceptResult.carbonize === true`, the accepted variant was stitched into
Do these five steps in the current thread, synchronously, before the next poll. Do not poll again until the file is clean.
1. **Locate the carbonize block** in the source file (`_acceptResult.file`). It's bracketed by `<!-- impeccable-carbonize-start SESSION_ID -->` and `<!-- impeccable-carbonize-end SESSION_ID -->` and contains a `<style data-impeccable-css="SESSION_ID">` element. If the variant declared parameters, an `<!-- impeccable-param-values SESSION_ID: {...} -->` comment sits alongside the style tag with the user's chosen values; read it first; it drives steps 3 and 4 below.
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `public/css/workflow.css` for this repo, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `site/styles/workflow.css` for an Astro project, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
3. **Bake in parameter values while rewriting selectors.** For `@scope ([data-impeccable-variant="N"])` wrappers: retarget to real, semantic classes on the accepted HTML (`.why-visual--v2 .v2-label { … }`). For `:scope[data-p-<id>="VALUE"]` selectors: keep only the branch matching the chosen value from the param-values comment; drop the others (they're dead after accept). For `var(--p-<id>, DEFAULT)` in the CSS: either substitute the literal value, or if the param is still useful as a knob going forward, leave the var and update its initial declaration to the chosen value.
4. **Unwrap the accepted content.** Delete the `<div data-impeccable-variant="N" style="display: contents">` that wraps it. Drop `data-impeccable-params` and any `data-p-*` attributes from it; those are live-mode plumbing, not source.
5. **Delete the inline `<style>` block, the `<!-- impeccable-param-values -->` comment if present, and both `<!-- impeccable-carbonize-start/end -->` markers.** Also drop any `@scope` rules for variants other than the accepted one; those are dead code now.
@@ -156,11 +156,11 @@ function broadcast(msg) {
function loadBrowserScripts() {
// Detection script: look relative to the skill scripts dir, then fall back
// to the npm package location (src/detect-antipatterns-browser.js).
// to the npm package location (cli/engine/detect-antipatterns-browser.js).
// This one IS cached — detect.js rarely changes during a session.
const detectPaths = [
path.join(__dirname, '..', '..', '..', '..', 'src', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'src', 'detect-antipatterns-browser.js'),
path.join(__dirname, '..', '..', '..', '..', 'cli', 'engine', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'cli', 'engine', 'detect-antipatterns-browser.js'),
];
let detectScript = '';
for (const p of detectPaths) {
@@ -411,7 +411,7 @@ When `_acceptResult.carbonize === true`, the accepted variant was stitched into
Do these five steps in the current thread, synchronously, before the next poll. Do not poll again until the file is clean.
1. **Locate the carbonize block** in the source file (`_acceptResult.file`). It's bracketed by `<!-- impeccable-carbonize-start SESSION_ID -->` and `<!-- impeccable-carbonize-end SESSION_ID -->` and contains a `<style data-impeccable-css="SESSION_ID">` element. If the variant declared parameters, an `<!-- impeccable-param-values SESSION_ID: {...} -->` comment sits alongside the style tag with the user's chosen values; read it first; it drives steps 3 and 4 below.
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `public/css/workflow.css` for this repo, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `site/styles/workflow.css` for an Astro project, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
3. **Bake in parameter values while rewriting selectors.** For `@scope ([data-impeccable-variant="N"])` wrappers: retarget to real, semantic classes on the accepted HTML (`.why-visual--v2 .v2-label { … }`). For `:scope[data-p-<id>="VALUE"]` selectors: keep only the branch matching the chosen value from the param-values comment; drop the others (they're dead after accept). For `var(--p-<id>, DEFAULT)` in the CSS: either substitute the literal value, or if the param is still useful as a knob going forward, leave the var and update its initial declaration to the chosen value.
4. **Unwrap the accepted content.** Delete the `<div data-impeccable-variant="N" style="display: contents">` that wraps it. Drop `data-impeccable-params` and any `data-p-*` attributes from it; those are live-mode plumbing, not source.
5. **Delete the inline `<style>` block, the `<!-- impeccable-param-values -->` comment if present, and both `<!-- impeccable-carbonize-start/end -->` markers.** Also drop any `@scope` rules for variants other than the accepted one; those are dead code now.
@@ -156,11 +156,11 @@ function broadcast(msg) {
function loadBrowserScripts() {
// Detection script: look relative to the skill scripts dir, then fall back
// to the npm package location (src/detect-antipatterns-browser.js).
// to the npm package location (cli/engine/detect-antipatterns-browser.js).
// This one IS cached — detect.js rarely changes during a session.
const detectPaths = [
path.join(__dirname, '..', '..', '..', '..', 'src', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'src', 'detect-antipatterns-browser.js'),
path.join(__dirname, '..', '..', '..', '..', 'cli', 'engine', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'cli', 'engine', 'detect-antipatterns-browser.js'),
];
let detectScript = '';
for (const p of detectPaths) {
+1 -1
View File
@@ -411,7 +411,7 @@ When `_acceptResult.carbonize === true`, the accepted variant was stitched into
Do these five steps in the current thread, synchronously, before the next poll. Do not poll again until the file is clean.
1. **Locate the carbonize block** in the source file (`_acceptResult.file`). It's bracketed by `<!-- impeccable-carbonize-start SESSION_ID -->` and `<!-- impeccable-carbonize-end SESSION_ID -->` and contains a `<style data-impeccable-css="SESSION_ID">` element. If the variant declared parameters, an `<!-- impeccable-param-values SESSION_ID: {...} -->` comment sits alongside the style tag with the user's chosen values; read it first; it drives steps 3 and 4 below.
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `public/css/workflow.css` for this repo, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `site/styles/workflow.css` for an Astro project, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
3. **Bake in parameter values while rewriting selectors.** For `@scope ([data-impeccable-variant="N"])` wrappers: retarget to real, semantic classes on the accepted HTML (`.why-visual--v2 .v2-label { … }`). For `:scope[data-p-<id>="VALUE"]` selectors: keep only the branch matching the chosen value from the param-values comment; drop the others (they're dead after accept). For `var(--p-<id>, DEFAULT)` in the CSS: either substitute the literal value, or if the param is still useful as a knob going forward, leave the var and update its initial declaration to the chosen value.
4. **Unwrap the accepted content.** Delete the `<div data-impeccable-variant="N" style="display: contents">` that wraps it. Drop `data-impeccable-params` and any `data-p-*` attributes from it; those are live-mode plumbing, not source.
5. **Delete the inline `<style>` block, the `<!-- impeccable-param-values -->` comment if present, and both `<!-- impeccable-carbonize-start/end -->` markers.** Also drop any `@scope` rules for variants other than the accepted one; those are dead code now.
@@ -156,11 +156,11 @@ function broadcast(msg) {
function loadBrowserScripts() {
// Detection script: look relative to the skill scripts dir, then fall back
// to the npm package location (src/detect-antipatterns-browser.js).
// to the npm package location (cli/engine/detect-antipatterns-browser.js).
// This one IS cached — detect.js rarely changes during a session.
const detectPaths = [
path.join(__dirname, '..', '..', '..', '..', 'src', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'src', 'detect-antipatterns-browser.js'),
path.join(__dirname, '..', '..', '..', '..', 'cli', 'engine', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'cli', 'engine', 'detect-antipatterns-browser.js'),
];
let detectScript = '';
for (const p of detectPaths) {
+1 -1
View File
@@ -411,7 +411,7 @@ When `_acceptResult.carbonize === true`, the accepted variant was stitched into
Do these five steps in the current thread, synchronously, before the next poll. Do not poll again until the file is clean.
1. **Locate the carbonize block** in the source file (`_acceptResult.file`). It's bracketed by `<!-- impeccable-carbonize-start SESSION_ID -->` and `<!-- impeccable-carbonize-end SESSION_ID -->` and contains a `<style data-impeccable-css="SESSION_ID">` element. If the variant declared parameters, an `<!-- impeccable-param-values SESSION_ID: {...} -->` comment sits alongside the style tag with the user's chosen values; read it first; it drives steps 3 and 4 below.
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `public/css/workflow.css` for this repo, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `site/styles/workflow.css` for an Astro project, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
3. **Bake in parameter values while rewriting selectors.** For `@scope ([data-impeccable-variant="N"])` wrappers: retarget to real, semantic classes on the accepted HTML (`.why-visual--v2 .v2-label { … }`). For `:scope[data-p-<id>="VALUE"]` selectors: keep only the branch matching the chosen value from the param-values comment; drop the others (they're dead after accept). For `var(--p-<id>, DEFAULT)` in the CSS: either substitute the literal value, or if the param is still useful as a knob going forward, leave the var and update its initial declaration to the chosen value.
4. **Unwrap the accepted content.** Delete the `<div data-impeccable-variant="N" style="display: contents">` that wraps it. Drop `data-impeccable-params` and any `data-p-*` attributes from it; those are live-mode plumbing, not source.
5. **Delete the inline `<style>` block, the `<!-- impeccable-param-values -->` comment if present, and both `<!-- impeccable-carbonize-start/end -->` markers.** Also drop any `@scope` rules for variants other than the accepted one; those are dead code now.
@@ -156,11 +156,11 @@ function broadcast(msg) {
function loadBrowserScripts() {
// Detection script: look relative to the skill scripts dir, then fall back
// to the npm package location (src/detect-antipatterns-browser.js).
// to the npm package location (cli/engine/detect-antipatterns-browser.js).
// This one IS cached — detect.js rarely changes during a session.
const detectPaths = [
path.join(__dirname, '..', '..', '..', '..', 'src', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'src', 'detect-antipatterns-browser.js'),
path.join(__dirname, '..', '..', '..', '..', 'cli', 'engine', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'cli', 'engine', 'detect-antipatterns-browser.js'),
];
let detectScript = '';
for (const p of detectPaths) {
+1 -1
View File
@@ -411,7 +411,7 @@ When `_acceptResult.carbonize === true`, the accepted variant was stitched into
Do these five steps in the current thread, synchronously, before the next poll. Do not poll again until the file is clean.
1. **Locate the carbonize block** in the source file (`_acceptResult.file`). It's bracketed by `<!-- impeccable-carbonize-start SESSION_ID -->` and `<!-- impeccable-carbonize-end SESSION_ID -->` and contains a `<style data-impeccable-css="SESSION_ID">` element. If the variant declared parameters, an `<!-- impeccable-param-values SESSION_ID: {...} -->` comment sits alongside the style tag with the user's chosen values; read it first; it drives steps 3 and 4 below.
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `public/css/workflow.css` for this repo, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `site/styles/workflow.css` for an Astro project, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
3. **Bake in parameter values while rewriting selectors.** For `@scope ([data-impeccable-variant="N"])` wrappers: retarget to real, semantic classes on the accepted HTML (`.why-visual--v2 .v2-label { … }`). For `:scope[data-p-<id>="VALUE"]` selectors: keep only the branch matching the chosen value from the param-values comment; drop the others (they're dead after accept). For `var(--p-<id>, DEFAULT)` in the CSS: either substitute the literal value, or if the param is still useful as a knob going forward, leave the var and update its initial declaration to the chosen value.
4. **Unwrap the accepted content.** Delete the `<div data-impeccable-variant="N" style="display: contents">` that wraps it. Drop `data-impeccable-params` and any `data-p-*` attributes from it; those are live-mode plumbing, not source.
5. **Delete the inline `<style>` block, the `<!-- impeccable-param-values -->` comment if present, and both `<!-- impeccable-carbonize-start/end -->` markers.** Also drop any `@scope` rules for variants other than the accepted one; those are dead code now.
@@ -156,11 +156,11 @@ function broadcast(msg) {
function loadBrowserScripts() {
// Detection script: look relative to the skill scripts dir, then fall back
// to the npm package location (src/detect-antipatterns-browser.js).
// to the npm package location (cli/engine/detect-antipatterns-browser.js).
// This one IS cached — detect.js rarely changes during a session.
const detectPaths = [
path.join(__dirname, '..', '..', '..', '..', 'src', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'src', 'detect-antipatterns-browser.js'),
path.join(__dirname, '..', '..', '..', '..', 'cli', 'engine', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'cli', 'engine', 'detect-antipatterns-browser.js'),
];
let detectScript = '';
for (const p of detectPaths) {
+1 -1
View File
@@ -411,7 +411,7 @@ When `_acceptResult.carbonize === true`, the accepted variant was stitched into
Do these five steps in the current thread, synchronously, before the next poll. Do not poll again until the file is clean.
1. **Locate the carbonize block** in the source file (`_acceptResult.file`). It's bracketed by `<!-- impeccable-carbonize-start SESSION_ID -->` and `<!-- impeccable-carbonize-end SESSION_ID -->` and contains a `<style data-impeccable-css="SESSION_ID">` element. If the variant declared parameters, an `<!-- impeccable-param-values SESSION_ID: {...} -->` comment sits alongside the style tag with the user's chosen values; read it first; it drives steps 3 and 4 below.
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `public/css/workflow.css` for this repo, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `site/styles/workflow.css` for an Astro project, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
3. **Bake in parameter values while rewriting selectors.** For `@scope ([data-impeccable-variant="N"])` wrappers: retarget to real, semantic classes on the accepted HTML (`.why-visual--v2 .v2-label { … }`). For `:scope[data-p-<id>="VALUE"]` selectors: keep only the branch matching the chosen value from the param-values comment; drop the others (they're dead after accept). For `var(--p-<id>, DEFAULT)` in the CSS: either substitute the literal value, or if the param is still useful as a knob going forward, leave the var and update its initial declaration to the chosen value.
4. **Unwrap the accepted content.** Delete the `<div data-impeccable-variant="N" style="display: contents">` that wraps it. Drop `data-impeccable-params` and any `data-p-*` attributes from it; those are live-mode plumbing, not source.
5. **Delete the inline `<style>` block, the `<!-- impeccable-param-values -->` comment if present, and both `<!-- impeccable-carbonize-start/end -->` markers.** Also drop any `@scope` rules for variants other than the accepted one; those are dead code now.
@@ -156,11 +156,11 @@ function broadcast(msg) {
function loadBrowserScripts() {
// Detection script: look relative to the skill scripts dir, then fall back
// to the npm package location (src/detect-antipatterns-browser.js).
// to the npm package location (cli/engine/detect-antipatterns-browser.js).
// This one IS cached — detect.js rarely changes during a session.
const detectPaths = [
path.join(__dirname, '..', '..', '..', '..', 'src', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'src', 'detect-antipatterns-browser.js'),
path.join(__dirname, '..', '..', '..', '..', 'cli', 'engine', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'cli', 'engine', 'detect-antipatterns-browser.js'),
];
let detectScript = '';
for (const p of detectPaths) {
+1 -1
View File
@@ -411,7 +411,7 @@ When `_acceptResult.carbonize === true`, the accepted variant was stitched into
Do these five steps in the current thread, synchronously, before the next poll. Do not poll again until the file is clean.
1. **Locate the carbonize block** in the source file (`_acceptResult.file`). It's bracketed by `<!-- impeccable-carbonize-start SESSION_ID -->` and `<!-- impeccable-carbonize-end SESSION_ID -->` and contains a `<style data-impeccable-css="SESSION_ID">` element. If the variant declared parameters, an `<!-- impeccable-param-values SESSION_ID: {...} -->` comment sits alongside the style tag with the user's chosen values; read it first; it drives steps 3 and 4 below.
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `public/css/workflow.css` for this repo, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `site/styles/workflow.css` for an Astro project, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
3. **Bake in parameter values while rewriting selectors.** For `@scope ([data-impeccable-variant="N"])` wrappers: retarget to real, semantic classes on the accepted HTML (`.why-visual--v2 .v2-label { … }`). For `:scope[data-p-<id>="VALUE"]` selectors: keep only the branch matching the chosen value from the param-values comment; drop the others (they're dead after accept). For `var(--p-<id>, DEFAULT)` in the CSS: either substitute the literal value, or if the param is still useful as a knob going forward, leave the var and update its initial declaration to the chosen value.
4. **Unwrap the accepted content.** Delete the `<div data-impeccable-variant="N" style="display: contents">` that wraps it. Drop `data-impeccable-params` and any `data-p-*` attributes from it; those are live-mode plumbing, not source.
5. **Delete the inline `<style>` block, the `<!-- impeccable-param-values -->` comment if present, and both `<!-- impeccable-carbonize-start/end -->` markers.** Also drop any `@scope` rules for variants other than the accepted one; those are dead code now.
@@ -156,11 +156,11 @@ function broadcast(msg) {
function loadBrowserScripts() {
// Detection script: look relative to the skill scripts dir, then fall back
// to the npm package location (src/detect-antipatterns-browser.js).
// to the npm package location (cli/engine/detect-antipatterns-browser.js).
// This one IS cached — detect.js rarely changes during a session.
const detectPaths = [
path.join(__dirname, '..', '..', '..', '..', 'src', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'src', 'detect-antipatterns-browser.js'),
path.join(__dirname, '..', '..', '..', '..', 'cli', 'engine', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'cli', 'engine', 'detect-antipatterns-browser.js'),
];
let detectScript = '';
for (const p of detectPaths) {
+8 -8
View File
@@ -2,11 +2,11 @@
## Skill bootstrap: skip the post-update-cleanup step
If you load the Impeccable skill from `.agents/skills/impeccable/SKILL.md` (or any sibling harness directory under this repo), it begins with a `<post-update-cleanup>` block that runs `scripts/cleanup-deprecated.mjs` and then self-deletes. **Do not run it here, and do not delete the block.** This repo is the source of truth for the skill; cleanup-deprecated assumes a consumer project and would touch legitimate source files. The block is regenerated from `source/skills/impeccable/SKILL.md` on every `bun run build`, so any deletion churns back. Skip it silently and proceed with the task.
If you load the Impeccable skill from `.agents/skills/impeccable/SKILL.md` (or any sibling harness directory under this repo), it begins with a `<post-update-cleanup>` block that runs `scripts/cleanup-deprecated.mjs` and then self-deletes. **Do not run it here, and do not delete the block.** This repo is the source of truth for the skill; cleanup-deprecated assumes a consumer project and would touch legitimate source files. The block is regenerated from `skill/SKILL.md` on every `bun run build`, so any deletion churns back. Skip it silently and proceed with the task.
## Project Structure & Module Organization
`source/` is the source of truth. Author skills in `source/skills/impeccable/` and keep provider output in `dist/` generated, not hand-edited. Build logic lives in `scripts/`, with provider configs in `scripts/lib/transformers/`. Runtime detection code ships from `src/`. The website lives in `public/`, local API/dev serving lives in `server/`, and regression coverage lives in `tests/` with fixtures under `tests/fixtures/`.
`source/` is the source of truth. Author skills in `skill/` and keep provider output in `dist/` generated, not hand-edited. Build logic lives in `scripts/`, with provider configs in `scripts/lib/transformers/`. Runtime detection code ships from `src/`. The website lives in `public/`, local API/dev serving lives in `server/`, and regression coverage lives in `tests/` with fixtures under `tests/fixtures/`.
## Build, Test, and Development Commands
@@ -37,13 +37,13 @@ Use ESM, semicolons, and the existing two-space indentation style in JS, HTML, a
Tests use Buns test runner plus Nodes built-in `--test`. Name tests `*.test.js` or `*.test.mjs` and place new fixtures near the behavior they cover, usually under `tests/fixtures/`. Prefer targeted test runs while iterating, then finish with `bun run test`. If you change generated outputs or provider transforms, verify both source parsing and at least one affected provider path in `dist/`.
For changes to `source/skills/impeccable/scripts/live-*.{mjs,js}`, also run `bun run test:live-e2e` (kept out of the default suite because it does real `npm install` per fixture and boots framework dev servers). Scope to one fixture with `IMPECCABLE_E2E_ONLY=<fixture-name>` while iterating; pass `IMPECCABLE_E2E_DEBUG=1` for page-DOM and dev-server-log dumps on failure. Schema and authoring guide for new fixtures live in `tests/framework-fixtures/README.md`.
For changes to `skill/scripts/live-*.{mjs,js}`, also run `bun run test:live-e2e` (kept out of the default suite because it does real `npm install` per fixture and boots framework dev servers). Scope to one fixture with `IMPECCABLE_E2E_ONLY=<fixture-name>` while iterating; pass `IMPECCABLE_E2E_DEBUG=1` for page-DOM and dev-server-log dumps on failure. Schema and authoring guide for new fixtures live in `tests/framework-fixtures/README.md`.
Set `IMPECCABLE_E2E_AGENT=llm` to swap the deterministic fake agent for a Claude-backed one (`tests/live-e2e/agents/llm-agent.mjs`, default Haiku 4.5, override via `IMPECCABLE_E2E_LLM_MODEL`). Requires `ANTHROPIC_API_KEY`; tests skip cleanly when it's unset. This path hits the API — use it for verification, not CI.
## Anti-pattern detection rules
`src/detect-antipatterns.mjs` is the source of truth for the rule engine. It feeds the CLI, the site overlay (`src/detect-antipatterns-browser.js`, regenerated by `bun run build:browser`), the Chrome extension (`extension/detector/`, regenerated by `bun run build:extension`), and the homepage `DETECTION_COUNT` in `public/js/generated/counts.js` (regenerated by `bun run build`). After any rule change run all three builds plus `bun run test` so nothing drifts.
`cli/engine/detect-antipatterns.mjs` is the source of truth for the rule engine. It feeds the CLI, the site overlay (`cli/engine/detect-antipatterns-browser.js`, regenerated by `bun run build:browser`), the Chrome extension (`extension/detector/`, regenerated by `bun run build:extension`), and the homepage `DETECTION_COUNT` in `site/public/js/generated/counts.js` (regenerated by `bun run build`). After any rule change run all three builds plus `bun run test` so nothing drifts.
TDD order is non-negotiable:
@@ -51,19 +51,19 @@ TDD order is non-negotiable:
2. Add a failing test in `tests/detect-antipatterns-fixtures.test.mjs` using the snippet-substring pattern (regex `/"([^"]+)"/` against `SHOULD_FLAG` / `SHOULD_PASS` lists).
3. Add the rule entry to the `ANTIPATTERNS` array (`id`, `category` = `slop` or `quality`, `name`, `description`, optional `skillSection` / `skillGuideline`).
4. Implement a pure `checkXxx(opts)` returning `[{ id, snippet }]` — no DOM access inside.
5. Add two adapters that wrap the pure check: `checkElementXxxDOM(el)` for the browser (`getComputedStyle` + `getBoundingClientRect`) and `checkElementXxx(el, tag, window)` for jsdom (`parseFloat(style.width)` instead of layout). Wire **both** adapters into **both** element loops in `src/detect-antipatterns.mjs` (browser loop ~line 1837, jsdom loop in `detectHtml` ~line 2058). Forgetting one is the most common mistake.
5. Add two adapters that wrap the pure check: `checkElementXxxDOM(el)` for the browser (`getComputedStyle` + `getBoundingClientRect`) and `checkElementXxx(el, tag, window)` for jsdom (`parseFloat(style.width)` instead of layout). Wire **both** adapters into **both** element loops in `cli/engine/detect-antipatterns.mjs` (browser loop ~line 1837, jsdom loop in `detectHtml` ~line 2058). Forgetting one is the most common mistake.
6. Verify on a live page at `http://localhost:3000/fixtures/antipatterns/{rule-id}.html` and on the homepage. The two adapter paths can disagree.
Conventions: wrap the identifying heading text in straight double quotes inside snippets so the fixture test can extract it. jsdom-specific helpers `resolveBackground()`, `resolveGradientStops()`, and `parseGradientColors()` exist because `background:` shorthand isn't decomposed and computed colors aren't normalized in jsdom — use them. Reference rules to copy from: `side-tab` (border), `low-contrast` (color+gradient), `icon-tile-stack` (sibling relationship), `flat-type-hierarchy` (page-level).
## Commit & Pull Request Guidelines
Recent history favors short, imperative subjects such as `Fix: ...`, `Add ...`, `Improve ...`, or `Bump ...`. Keep commits focused and explain the user-facing impact when it is not obvious. PRs should summarize what changed, list validation performed, and call out regenerated artifacts like `dist/` or `build/`. Include screenshots for visible `public/` changes and mention affected providers when transform behavior changes.
Recent history favors short, imperative subjects such as `Fix: ...`, `Add ...`, `Improve ...`, or `Bump ...`. Keep commits focused and explain the user-facing impact when it is not obvious. PRs should summarize what changed, list validation performed, and call out regenerated artifacts like `dist/` or `build/`. Include screenshots for visible `site/` changes and mention affected providers when transform behavior changes.
## Releases
Tags are per-component because the three components ship independently: `skill-v` (`.claude-plugin/plugin.json` + `.claude-plugin/marketplace.json`), `cli-v` (`package.json`), `ext-v` (`extension/manifest.json`). Flow: bump the relevant manifest, add a changelog entry to `public/index.html` (skill = bare `vX.Y.Z`; CLI = `CLI vX.Y.Z`; extension = `Extension vX.Y.Z` — the prefix is how `scripts/release.mjs` finds the right block), commit, push, then `bun run release:<skill|cli|ext>` (or `--dry-run` first). The script refuses on a dirty tree, an unpushed HEAD, a missing changelog entry, or stale build outputs; skill and extension reruns of `bun run build` / `bun run build:extension` must produce zero diff. Skill releases attach `dist/universal.zip`; extension releases attach `dist/extension.zip`. CLI ships to npm via a separate `npm publish`, and the extension zip uploads to the Chrome Web Store manually — both reminded at the end of the script. Fix already-shipped notes with `gh release edit <tag> --notes-file <md>`.
Tags are per-component because the three components ship independently: `skill-v` (`.claude-plugin/plugin.json` + `.claude-plugin/marketplace.json`), `cli-v` (`package.json`), `ext-v` (`extension/manifest.json`). Flow: bump the relevant manifest, add a changelog entry to `site/pages/index.astro` (skill = bare `vX.Y.Z`; CLI = `CLI vX.Y.Z`; extension = `Extension vX.Y.Z` — the prefix is how `scripts/release.mjs` finds the right block), commit, push, then `bun run release:<skill|cli|ext>` (or `--dry-run` first). The script refuses on a dirty tree, an unpushed HEAD, a missing changelog entry, or stale build outputs; skill and extension reruns of `bun run build` / `bun run build:extension` must produce zero diff. Skill releases attach `dist/universal.zip`; extension releases attach `dist/extension.zip`. CLI ships to npm via a separate `npm publish`, and the extension zip uploads to the Chrome Web Store manually — both reminded at the end of the script. Fix already-shipped notes with `gh release edit <tag> --notes-file <md>`.
## Contributor Notes
Do not edit generated provider files directly unless you are intentionally patching generated output as part of a build-system change. Prefer fixing the root source in `source/`, `scripts/`, or `src/`, then regenerate artifacts.
Do not edit generated provider files directly unless you are intentionally patching generated output as part of a build-system change. Prefer fixing the root source in `skill/`, `scripts/`, or `cli/`, then regenerate artifacts.
+45 -45
View File
@@ -2,13 +2,13 @@
## Skill bootstrap: skip the post-update-cleanup step
The `<post-update-cleanup>` block at the top of `.claude/skills/impeccable/SKILL.md` runs `scripts/cleanup-deprecated.mjs` and then self-deletes. **Do not run it in this repo, and do not delete the block.** This repo is the source of truth for the Impeccable skill; the cleanup script scans for "leftover" skill files to remove and its judgment doesn't fit a repo where the skill lives in `source/`. The block is also regenerated from `source/skills/impeccable/SKILL.md` on every `bun run build`, so any deletion churns back on the next build. Skip the step silently and proceed.
The `<post-update-cleanup>` block at the top of `.claude/skills/impeccable/SKILL.md` runs `scripts/cleanup-deprecated.mjs` and then self-deletes. **Do not run it in this repo, and do not delete the block.** This repo is the source of truth for the Impeccable skill; the cleanup script scans for "leftover" skill files to remove and its judgment doesn't fit a repo where the skill lives in `skill/`. The block is also regenerated from `skill/SKILL.md` on every `bun run build`, so any deletion churns back on the next build. Skip the step silently and proceed.
Same rule for AGENTS.md and every other harness-specific instruction file: treat post-update-cleanup as a no-op in this repo.
## Architecture (v3.0+)
There is **one** user-invocable skill, `impeccable`, with **23 commands** underneath it. Users type `/impeccable polish`, `/impeccable audit`, etc. The skill is defined in `source/skills/impeccable/`:
There is **one** user-invocable skill, `impeccable`, with **23 commands** underneath it. Users type `/impeccable polish`, `/impeccable audit`, etc. The skill is defined in `skill/`:
- `SKILL.md` — frontmatter (with the auto-trigger-optimized description and the `allowed-tools` list), shared design laws, and the **Commands** router table.
- `reference/` — one `<command>.md` per command (`audit.md`, `polish.md`, `critique.md`, etc.) plus the domain reference files (`typography.md`, `color-and-contrast.md`, etc.). When a sub-command is matched, the router loads its reference file.
@@ -34,15 +34,16 @@ Sub-command reference files add a short `## Register` section near the top *only
## CSS
Plain hand-written CSS, no Tailwind, no build step. Bun's HTML loader resolves `<link rel="stylesheet">` and inlines `@import` chains automatically for both `bun run dev` and `bun run build`.
Plain hand-written CSS, no Tailwind. Imported into Astro pages/layouts via frontmatter `import` statements; Vite resolves `@import` chains automatically.
The CSS architecture:
- `public/css/main.css` — Main entry point, imports the partials and defines tokens/reset
- `public/css/workflow.css` — Commands section, glass terminal, magazine spread styles
- `public/css/sub-pages.css``/docs`, `/anti-patterns`, `/tutorials`, detail pages
- `public/css/tokens.css` — OKLCH color tokens (ink, charcoal, ash, mist, cream, accent)
The CSS architecture (under `site/styles/`):
- `main.css` — Main entry point, imports the partials and defines tokens/reset
- `workflow.css` — Commands section, glass terminal, magazine spread styles
- `sub-pages.css``/docs`, `/anti-patterns`, `/tutorials`, detail pages
- `tokens.css` — OKLCH color tokens (ink, charcoal, ash, mist, cream, accent)
- `footer.css` — shared across all pages, imported in `Base.astro`
Edit any of these directly and reload. No rebuild needed for CSS changes.
Edit any of these directly and the dev server hot-reloads. No rebuild needed for CSS changes.
## Color token rule
@@ -57,17 +58,16 @@ Editorial brief is at `STYLE.md` (root). Read it before editing the homepage, su
The build's `validateProse` step (in `scripts/build.js`) enforces a denylist: em dashes (`—` and HTML entities), the `--` em-dash substitute, `load-bearing`, `highest-leverage`, `biggest unlock`, `seamless`, `robust`, `delve`, `elevate`, `empower`, `underscore`, `pivotal`, `tapestry`, `data-driven`, `reflex defaults`, `collapses into monoculture`, `in today's`, `gone are the days`, `whether you're`, `let's dive in`, `in summary`, `in conclusion`, `moreover`, `furthermore`. Each rule prints a rationale and a suggested replacement when it fires. **Do not silently work around the regex.** If a banned word has earned a real meaning here, raise it as a STYLE.md amendment.
The validator scans `content/site/`, `site/pages/`, `site/content/`, `site/components/`, `site/layouts/`, `README.md`, `README.npm.md`. It deliberately skips `source/skills/impeccable/` because LLM-facing reference instructions sometimes need technical phrasings the marketing copy can't.
The validator scans `site/pages/`, `site/content/`, `site/components/`, `site/layouts/`, `README.md`, `README.npm.md`. It deliberately skips `skill/` because LLM-facing reference instructions sometimes need technical phrasings the marketing copy can't.
The deeper structural issues (negation pivot, triadic auto-pilot, uniform paragraph rhythm, hollow confidence) require human judgment. STYLE.md lists them. Use them on every editorial pass.
## Two content trees: keep them in sync
## Editorial content lives under `site/content/`
After the Astro migration, editorials and tutorials live in TWO places that are both real:
- `content/site/skills/<id>.md` and `content/site/tutorials/<id>.md` — read by `scripts/build.js` for taglines and as source of truth for downstream tooling.
- `site/content/skills/<id>.md` and `site/content/tutorials/<id>.md`read by Astro's content collection; this is what actually renders on the site.
There is no automated sync. **Edit both** when changing any editorial or tutorial body. `diff -rq content/site/ site/content/` should always be clean except for `anti-patterns-catalog.js`. Unifying these is on the cleanup list.
Skill editorials and tutorials are read by `scripts/build.js` (for taglines and downstream tooling) and by Astro's content collection (for what actually renders on the site). One tree, one place to edit:
- `site/content/skills/<id>.md` — optional editorial wrapper with frontmatter `tagline` plus body sections
- `site/content/tutorials/<slug>.md`full tutorial content
- `site/data/anti-patterns-catalog.js` — detection-rule catalog (visual examples, gallery items, layer definitions)
## Development Server
@@ -76,9 +76,9 @@ bun run dev # Bun dev server at http://localhost:3000
bun run preview # Build + Cloudflare Pages local preview
```
The dev server (in `server/index.js`) runs `generateSubPages` at module load, so editing source files in `content/site/skills/`, `source/skills/impeccable/`, or the sub-page generator requires a **server restart** (not just a browser reload) to see the change. CSS hot-reloads fine without a restart.
The dev server runs Astro (`astro dev`). Editing files in `site/content/skills/`, `skill/`, or `scripts/lib/sub-pages-data.js` requires a **server restart** (not just a browser reload) to see the change. CSS, components, and pages hot-reload fine without a restart.
**Legacy URL redirects** live in `server/index.js` and must stay in sync with `scripts/build.js` `_redirects` generation. Current redirects: `/skills``/docs`, `/skills/:id``/docs/:id`, `/cheatsheet``/docs`, `/gallery``/visual-mode#try-it-live`.
**Legacy URL redirects** are emitted to `_redirects` by `scripts/build.js` (via `generateCFConfig`); the dynamic `/skills/:id → /docs/:id` redirect lives in `site/public/_redirects` (Cloudflare Pages reads both at deploy). Current redirects: `/skills``/docs`, `/skills/:id``/docs/:id`, `/cheatsheet``/docs`, `/gallery``/visual-mode#try-it-live`.
## Deployment
@@ -90,7 +90,7 @@ bun run deploy # Build + deploy to Cloudflare Pages
## Build System
The build system compiles the impeccable skill from `source/` to provider-specific formats in `dist/`:
The build system compiles the impeccable skill from `skill/` to provider-specific formats in `dist/`:
```bash
bun run build # Build all providers
@@ -107,13 +107,13 @@ Source files use placeholders that get replaced per-provider:
### Harness output directories are tracked
`.claude/skills/`, `.cursor/skills/`, `.agents/skills/`, and the other 8 harness directories are **intentionally committed to the repo**. `npx skills` reads them directly from this repo at install time, and they enable clean submodule use. Do not gitignore them. Run `bun run build` to refresh them after editing `source/skills/`.
`.claude/skills/`, `.cursor/skills/`, `.agents/skills/`, and the other 8 harness directories are **intentionally committed to the repo**. `npx skills` reads them directly from this repo at install time, and they enable clean submodule use. Do not gitignore them. Run `bun run build` to refresh them after editing `skill/`.
Local state files inside harness directories (e.g. `.claude/scheduled_tasks.lock`, `.claude/settings.local.json`) ARE gitignored.
### Generated sub-pages are gitignored
`public/docs/`, `public/anti-patterns/`, `public/tutorials/`, `public/visual-mode/` are generated by `scripts/build-sub-pages.js` on dev server startup and during `bun run build`. They're gitignored because the production site (Cloudflare Pages) runs its own build and nobody consumes them directly from git.
`site/public/docs/`, `site/public/anti-patterns/`, `site/public/tutorials/`, `site/public/visual-mode/`, `site/public/slop/` are gitignored as legacy generator output paths. Astro's content collections drive the live site under `site/pages/docs/`, `site/pages/tutorials/`, etc.; nothing reads from those gitignored dirs anymore.
## Testing
@@ -138,7 +138,7 @@ IMPECCABLE_E2E_DEBUG=1 bun run test:live-e2e # dump page DOM + de
**One-time setup**: `npx playwright install chromium` (the suite uses a specific Chromium build keyed to the bundled Playwright version).
**Kept out of the default `bun run test`** because (a) it does real `npm install` per fixture, (b) it boots framework dev servers, (c) wall time is ~2 minutes, and (d) it requires Playwright's browser cache. Run it locally before shipping changes to anything in `source/skills/impeccable/scripts/live-*.{mjs,js}`.
**Kept out of the default `bun run test`** because (a) it does real `npm install` per fixture, (b) it boots framework dev servers, (c) wall time is ~2 minutes, and (d) it requires Playwright's browser cache. Run it locally before shipping changes to anything in `skill/scripts/live-*.{mjs,js}`.
The agent is pluggable via a one-method interface in `tests/live-e2e/agent.mjs`: `generateVariants(event, context) → { scopedCss, variants[] }`. The default fake agent emits canned variants that exercise all three param kinds (`range`, `steps`, `toggle`). The orchestrator (wrap, write, accept, carbonize) is agent-agnostic.
@@ -148,7 +148,7 @@ Adding a new fixture is a matter of cloning a directory under `tests/framework-f
## CLI
The CLI lives in this repo under `bin/` and `src/`. Published to npm as `impeccable`.
The CLI lives in this repo under `cli/`: `cli/bin/` (entry + sub-commands), `cli/engine/` (the detect-antipatterns rule engine + browser variant), `cli/lib/` (helpers shared by CLI and Cloudflare Pages Functions). Published to npm as `impeccable`.
```bash
npx impeccable detect [file-or-dir-or-url...] # detect anti-patterns
@@ -158,7 +158,7 @@ npx impeccable skills install # install skills
npx impeccable --help # show help
```
The browser detector (`src/detect-antipatterns-browser.js`) is generated from the main engine. After changing `src/detect-antipatterns.mjs`, rebuild it:
The browser detector (`cli/engine/detect-antipatterns-browser.js`) is generated from the main engine. After changing `cli/engine/detect-antipatterns.mjs`, rebuild it:
```bash
bun run build:browser
@@ -172,18 +172,18 @@ There are three independently versioned components. Only bump the one(s) that ac
**CLI** (npm package):
- `package.json``version`
- Bump when: CLI code changes (`bin/`, `src/detect-antipatterns.mjs`, etc.)
- Bump when: CLI code changes (`cli/bin/`, `cli/engine/detect-antipatterns.mjs`, etc.)
**Skills** (Claude Code plugin / skill definitions):
- `.claude-plugin/plugin.json``version`
- `.claude-plugin/marketplace.json``plugins[0].version`
- Bump when: skill content changes (`source/skills/`, reference files, command metadata, etc.)
- Bump when: skill content changes (`skill/`, reference files, command metadata, etc.)
**Chrome extension**:
- `extension/manifest.json``version`
- Bump when: extension code changes (`extension/`)
**Website changelog** (`public/index.html`):
**Website changelog** (`site/pages/index.astro`):
- Hero version link text + new changelog entry in the changelog section
- Update for user-facing changes only, not internal build/tooling details
- Use the most prominent version that changed (skills version is usually the right one)
@@ -197,7 +197,7 @@ GitHub releases are tagged per-component, not per-version, since the three compo
Workflow for any component:
1. Bump the manifest version (see Versioning above).
2. Add a changelog entry to `public/index.html`. Skill entries use a bare `vX.Y.Z` label; CLI and extension entries use the prefixed forms `CLI vX.Y.Z` and `Extension vX.Y.Z`. The release script extracts notes by matching this label, so the prefix matters.
2. Add a changelog entry to `site/pages/index.astro`. Skill entries use a bare `vX.Y.Z` label; CLI and extension entries use the prefixed forms `CLI vX.Y.Z` and `Extension vX.Y.Z`. The release script extracts notes by matching this label, so the prefix matters.
3. Commit and push to `main`.
4. Run `bun run release:<skill|cli|ext>`. Preview first with `node scripts/release.mjs <component> --dry-run`.
@@ -211,22 +211,22 @@ If you need to fix release notes after the fact (typo, missing thank-you, format
All commands live under `/impeccable`. To add a new one:
1. Create `source/skills/impeccable/reference/<command>.md` with the command's instructions (this is what the LLM loads when the command is invoked)
2. Add a row to the **Sub-command reference table** in `source/skills/impeccable/SKILL.md`
1. Create `skill/reference/<command>.md` with the command's instructions (this is what the LLM loads when the command is invoked)
2. Add a row to the **Sub-command reference table** in `skill/SKILL.md`
3. Add an entry to the **Command menu** section in the same file
4. Add the command name to `IMPECCABLE_SUB_COMMANDS` in `scripts/lib/utils.js`
5. Add it to `VALID_COMMANDS` in `source/skills/impeccable/scripts/pin.mjs`
6. Add its metadata (description + argumentHint) to `source/skills/impeccable/scripts/command-metadata.json`
5. Add it to `VALID_COMMANDS` in `skill/scripts/pin.mjs`
6. Add its metadata (description + argumentHint) to `skill/scripts/command-metadata.json`
7. Add its category to `SKILL_CATEGORIES` in `scripts/lib/sub-pages-data.js`
8. Add its relationships (leadsTo / pairs / combinesWith) to `COMMAND_RELATIONSHIPS` in the same file
9. Add the same category entry to `public/js/data.js` `commandCategories` and `commandProcessSteps` (for the homepage carousel)
10. Add symbol + number to `commandSymbols` and `commandNumbers` in `public/js/components/framework-viz.js` (periodic table)
11. Optional: write an editorial wrapper at `content/site/skills/<command>.md` with a short `tagline` and expanded body (When to use it / How it works / Try it / Pitfalls)
9. Add the same category entry to `site/scripts/data.js` `commandCategories` and `commandProcessSteps` (for the homepage carousel)
10. Add symbol + number to `commandSymbols` and `commandNumbers` in `site/scripts/components/framework-viz.js` (periodic table)
11. Optional: write an editorial wrapper at `site/content/skills/<command>.md` with a short `tagline` and expanded body (When to use it / How it works / Try it / Pitfalls)
The build system counts commands from the router table automatically. Update the command count in **all** of these locations when the total changes:
- `public/index.html` — meta descriptions, hero box, section lead
- `public/cheatsheet.html` does not exist anymore; `/cheatsheet` redirects to `/docs`
- `site/pages/index.astro` — meta descriptions, hero box, section lead
- `/cheatsheet` redirects to `/docs` (no standalone page)
- `README.md` — intro, command count, commands table
- `NOTICE.md` — command count
- `AGENTS.md` — intro command count
@@ -237,7 +237,7 @@ The build validator (`generateCounts` in `scripts/build.js`) checks these files
## Adding editorial content for existing commands
Editorial files live at `content/site/skills/<command>.md` and have a `tagline` frontmatter plus a body with the standard four sections:
Editorial files live at `site/content/skills/<command>.md` and have a `tagline` frontmatter plus a body with the standard four sections:
- **When to use it** — the specific scenarios this command owns
- **How it works** — the internal process, phases, or approach
@@ -250,15 +250,15 @@ Every command should have an editorial file eventually, but the build does not r
## Adding or modifying anti-pattern detection rules
`src/detect-antipatterns.mjs` is the source of truth for the rule engine. It powers the CLI, the public-site overlay, the Chrome extension, and the homepage rule count. Five places stay in sync:
`cli/engine/detect-antipatterns.mjs` is the source of truth for the rule engine. It powers the CLI, the public-site overlay, the Chrome extension, and the homepage rule count. Five places stay in sync:
| Where | How it stays in sync |
|---|---|
| `src/detect-antipatterns.mjs` (`ANTIPATTERNS` array + `checkXxx` logic) | Hand-edited |
| `src/detect-antipatterns-browser.js` | `bun run build:browser` |
| `cli/engine/detect-antipatterns.mjs` (`ANTIPATTERNS` array + `checkXxx` logic) | Hand-edited |
| `cli/engine/detect-antipatterns-browser.js` | `bun run build:browser` |
| `extension/detector/detect.js` + `extension/detector/antipatterns.json` | `bun run build:extension` |
| `public/js/generated/counts.js` (`DETECTION_COUNT`) | `bun run build` |
| `source/skills/impeccable/SKILL.md` and `reference/*.md` | Hand-edited if the rule introduces new design guidance |
| `site/public/js/generated/counts.js` (`DETECTION_COUNT`) | `bun run build` |
| `skill/SKILL.md` and `reference/*.md` | Hand-edited if the rule introduces new design guidance |
Always run all three builds and the test suite after a rule change:
@@ -272,7 +272,7 @@ bun run build && bun run build:browser && bun run build:extension && bun run tes
2. **Failing test** in `tests/detect-antipatterns-fixtures.test.mjs` using the snippet-substring pattern (regex `/"([^"]+)"/` against `SHOULD_FLAG` / `SHOULD_PASS` lists). Run it and watch it fail before implementing.
3. **Rule entry** in the `ANTIPATTERNS` array: `id`, `category` (`slop` for AI tells, `quality` for real design or a11y issues), `name`, `description`, optional `skillSection` and `skillGuideline`.
4. **Pure check function** `checkXxx(opts)` returning `[{ id, snippet }]`. No DOM access in the pure function.
5. **Two adapters**: `checkElementXxxDOM(el)` for the browser (`getComputedStyle` + `getBoundingClientRect`) and `checkElementXxx(el, tag, window)` for jsdom (`parseFloat(style.width)` instead of layout). Wire **both** into **both** element loops in `src/detect-antipatterns.mjs` — the browser loop (~line 1837) and the jsdom loop in `detectHtml` (~line 2058). Forgetting one is the most common mistake; symptom is "test passes, live page silent" or vice versa.
5. **Two adapters**: `checkElementXxxDOM(el)` for the browser (`getComputedStyle` + `getBoundingClientRect`) and `checkElementXxx(el, tag, window)` for jsdom (`parseFloat(style.width)` instead of layout). Wire **both** into **both** element loops in `cli/engine/detect-antipatterns.mjs` — the browser loop (~line 1837) and the jsdom loop in `detectHtml` (~line 2058). Forgetting one is the most common mistake; symptom is "test passes, live page silent" or vice versa.
6. **Verify on a live page**: `http://localhost:3000/fixtures/antipatterns/{rule-id}.html` and the homepage (no false positives). The two adapter paths can disagree, so manual browser checks catch what the fixture test can't.
### Conventions and jsdom gotchas
@@ -295,7 +295,7 @@ cd ~/code/impeccable-evals
bun run serve # dashboard on http://localhost:8723
```
The eval runners read this repo's skill from `../impeccable/source/skills/impeccable/` and staged provider skills from `../impeccable/build/_data/dist/*`. Run `bun run build` in this repo before an eval sweep if you want the Claude/Gemini staged skills to reflect your latest edits.
The eval runners read this repo's skill from `../impeccable/skill/` and staged provider skills from `../impeccable/build/_data/dist/*`. Run `bun run build` in this repo before an eval sweep if you want the Claude/Gemini staged skills to reflect your latest edits.
### After structural skill changes, update `inline-skill.ts` in the evals repo
+3 -3
View File
@@ -4,13 +4,13 @@ Documentation for contributors to Impeccable.
## Architecture
Source skills in `source/skills/` are transformed into provider-specific formats by a config-driven factory. Each provider is defined as a config object in `scripts/lib/transformers/providers.js` -- adding a new provider requires only a new config entry.
The skill at `skill/` is transformed into provider-specific formats by a config-driven factory. Each provider is defined as a config object in `scripts/lib/transformers/providers.js` -- adding a new provider requires only a new config entry.
For detailed harness capabilities (which frontmatter fields each supports, placeholder systems, directory structures), see [HARNESSES.md](HARNESSES.md).
## Source Format
### Skills (`source/skills/{name}/SKILL.md`)
### Skill (`skill/SKILL.md`)
```yaml
---
@@ -127,7 +127,7 @@ scripts/
- `createTransformer(config)`: Factory that returns a transformer function from a provider config
- `parseFrontmatter()`: Extracts YAML frontmatter and body from SKILL.md files
- `readSourceFiles()`: Reads all skill directories from `source/skills/`
- `readSourceFiles()`: Reads `skill/SKILL.md` plus its `reference/` and `scripts/` siblings
- `replacePlaceholders()`: Substitutes `{{model}}`, `{{config_file}}`, etc. per provider
- `generateYamlFrontmatter()`: Serializes objects to YAML frontmatter (auto-quotes values starting with `[` or `{`)
+9 -9
View File
@@ -11,7 +11,7 @@ Anthropic's [frontend-design](https://github.com/anthropics/skills/tree/main/ski
Every model trained on the same SaaS templates. Skip the guidance and you get the same handful of tells on every project: Inter for everything, purple-to-blue gradients, cards nested in cards, gray text on colored backgrounds, the rounded-square icon tile above every heading.
Impeccable adds:
- **7 domain reference files** ([view source](source/skills/impeccable/)). Typography, color, motion, spatial, interaction, responsive, UX writing. Load on every command, alongside a brand-vs-product register that adjusts the defaults.
- **7 domain reference files** ([view source](skill/)). Typography, color, motion, spatial, interaction, responsive, UX writing. Load on every command, alongside a brand-vs-product register that adjusts the defaults.
- **23 commands.** A shared design vocabulary with your AI: `polish`, `audit`, `critique`, `distill`, `animate`, `bolder`, `quieter`, and more.
- **27 deterministic anti-pattern rules** plus a 12-rule LLM critique pass. CLI and browser extension run the deterministic ones with no LLM and no API key. Each is tied to specific design guidance the skill teaches against.
@@ -19,17 +19,17 @@ Impeccable adds:
### The Skill: impeccable
A comprehensive design skill with 7 domain-specific references ([view skill](source/skills/impeccable/SKILL.md)):
A comprehensive design skill with 7 domain-specific references ([view skill](skill/SKILL.md)):
| Reference | Covers |
|-----------|--------|
| [typography](source/skills/impeccable/reference/typography.md) | Type systems, font pairing, modular scales, OpenType |
| [color-and-contrast](source/skills/impeccable/reference/color-and-contrast.md) | OKLCH, tinted neutrals, dark mode, accessibility |
| [spatial-design](source/skills/impeccable/reference/spatial-design.md) | Spacing systems, grids, visual hierarchy |
| [motion-design](source/skills/impeccable/reference/motion-design.md) | Easing curves, staggering, reduced motion |
| [interaction-design](source/skills/impeccable/reference/interaction-design.md) | Forms, focus states, loading patterns |
| [responsive-design](source/skills/impeccable/reference/responsive-design.md) | Mobile-first, fluid design, container queries |
| [ux-writing](source/skills/impeccable/reference/ux-writing.md) | Button labels, error messages, empty states |
| [typography](skill/reference/typography.md) | Type systems, font pairing, modular scales, OpenType |
| [color-and-contrast](skill/reference/color-and-contrast.md) | OKLCH, tinted neutrals, dark mode, accessibility |
| [spatial-design](skill/reference/spatial-design.md) | Spacing systems, grids, visual hierarchy |
| [motion-design](skill/reference/motion-design.md) | Easing curves, staggering, reduced motion |
| [interaction-design](skill/reference/interaction-design.md) | Forms, focus states, loading patterns |
| [responsive-design](skill/reference/responsive-design.md) | Mobile-first, fluid design, container queries |
| [ux-writing](skill/reference/ux-writing.md) | Button labels, error messages, empty states |
### 23 Commands
+1
View File
@@ -2,6 +2,7 @@ import { defineConfig } from 'astro/config';
export default defineConfig({
srcDir: './site',
publicDir: './site/public',
output: 'static',
build: {
format: 'directory',
+3 -3
View File
@@ -36,14 +36,14 @@ Run 'impeccable <command> --help' for command-specific options.`);
}
if (command === '--version' || command === '-v') {
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
const pkg = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf8'));
console.log(pkg.version);
process.exit(0);
}
if (command === 'detect') {
process.argv = [process.argv[0], process.argv[1], ...args.slice(1)];
const { detectCli } = await import('../src/detect-antipatterns.mjs');
const { detectCli } = await import('../engine/detect-antipatterns.mjs');
await detectCli();
} else if (command === 'skills') {
const { run } = await import('./commands/skills.mjs');
@@ -51,6 +51,6 @@ if (command === 'detect') {
} else {
// Default: treat as detect arguments (allow `npx impeccable src/` shorthand)
process.argv = [process.argv[0], process.argv[1], ...args];
const { detectCli } = await import('../src/detect-antipatterns.mjs');
const { detectCli } = await import('../engine/detect-antipatterns.mjs');
await detectCli();
}
@@ -364,7 +364,7 @@ async function install(flags) {
// Clean up deprecated skills from previous versions
try {
const { cleanup } = await import('../../source/skills/impeccable/scripts/cleanup-deprecated.mjs');
const { cleanup } = await import('../../../skill/scripts/cleanup-deprecated.mjs');
const result = cleanup(root);
const total = result.deletedPaths.length + result.removedLockEntries.length;
if (total > 0) {
@@ -513,7 +513,7 @@ async function update(flags = []) {
// Clean up deprecated skills from previous versions.
try {
const { cleanup } = await import('../../source/skills/impeccable/scripts/cleanup-deprecated.mjs');
const { cleanup } = await import('../../../skill/scripts/cleanup-deprecated.mjs');
const root = findProjectRoot();
const result = cleanup(root);
const total = result.deletedPaths.length + result.removedLockEntries.length;
@@ -598,7 +598,7 @@ async function update(flags = []) {
// Run cleanup to remove deprecated stubs from the fresh download
try {
const { cleanup: postCleanup } = await import('../../source/skills/impeccable/scripts/cleanup-deprecated.mjs');
const { cleanup: postCleanup } = await import('../../../skill/scripts/cleanup-deprecated.mjs');
postCleanup(root);
} catch {
// Not available -- skip
-40
View File
@@ -1,40 +0,0 @@
---
tagline: "Make designs work across screens, devices, and contexts without amputating features."
---
## When to use it
`/impeccable adapt` is for taking a design built for one context and making it work in another. Mobile from desktop, tablet from mobile, print from web, embedded from standalone, email from dashboard. Reach for it when the source design is solid but falls apart at other breakpoints, on touch, or in a different container.
Not for building responsive from scratch. For that, start with `/impeccable` and shape the layout responsive-first. Adapt is for the "we never thought about mobile" backfill.
## How it works
The skill works through four dimensions of contextual fit:
1. **Breakpoints and fluid layout**: collapse multi-column to single, adjust clamp ranges, introduce new breakpoints where the design genuinely breaks.
2. **Touch targets**: minimum 44px hit areas, sufficient spacing between adjacent targets, larger tap zones than visual bounds where needed.
3. **Navigation patterns**: desktop sidebars become mobile bottom nav or slide-outs, dense toolbars collapse into menus, hover states get touch equivalents.
4. **Content priority**: decide what must be visible, what can collapse into disclosures, what can be removed entirely for that context.
The non-negotiable rule: adapt, do not amputate. Critical functionality cannot disappear on mobile just because it is inconvenient. Find a way to fit it, redesign the interaction, or reconsider whether it was really critical on desktop.
## Try it
```
/impeccable adapt the settings page for mobile
```
Expected changes:
- Three-column grid becomes single column with section headers acting as sticky dividers
- Sidebar nav moves to a horizontal scroller above the content
- Toggles gain 8px vertical padding so they meet 44px touch targets
- Inline help text moves to tap-to-reveal, not hover
- The "Danger zone" section expands fully on mobile instead of collapsing, because it contains irreversible actions and we want users to see them clearly
## Pitfalls
- **Amputating features.** If the mobile version hides things the desktop version can do, that is a regression, not an adaptation. Fight for the feature.
- **Treating mobile as "smaller desktop".** Mobile is a different context: thumbs, interruption, short sessions. Adapt to the context, not to the viewport width.
- **Skipping `/impeccable harden` afterward.** Responsive layouts reveal edge cases. Run hardening after adapt to catch the ones that only show up at 320px.
-42
View File
@@ -1,42 +0,0 @@
---
tagline: "Purposeful motion that conveys state, not decoration."
---
## When to use it
`/impeccable animate` is for interfaces that feel lifeless, where state changes are instant and jarring, where loading just pops in, where the user never quite trusts that their click registered. Use it to add the small motions that communicate what is happening: entrances, exits, feedback, transitions between states.
Do not use it to add bounces or elastic springs for the sake of energy. That is decoration, and this skill will not give it to you.
## How it works
The skill identifies static moments that would benefit from motion, then applies them with strict discipline:
1. **Entrances and exits**: elements appear and leave with 200 to 300ms fades plus subtle Y or scale, never layout properties.
2. **State feedback**: hover, active, focus, loading, success all communicate via motion instead of sudden swaps.
3. **Transitions between views**: shared-element transitions where it makes sense, fade-through otherwise.
4. **Progress and loading**: skeleton screens, determinate bars, motion that says "still working".
5. **Reduced motion**: every animation has a `prefers-reduced-motion` fallback.
Easing is always exponential (ease-out-quart, quint, or expo) because real objects decelerate smoothly. No bounce, no elastic, no linear for anything except progress indicators.
The skill animates `transform` and `opacity` only. If you find yourself animating `width`, `height`, `top`, or `left`, it is doing the wrong thing. Use `grid-template-rows` for height transitions.
## Try it
```
/impeccable animate the sign-up flow
```
Typical additions:
- Email input gets a focus glow on focus-visible (opacity + shadow, 180ms)
- Submit button shows a spinner inside itself on loading state, not a separate spinner next to it
- Success screen enters with opacity + translateY(8px), 260ms, ease-out-quart
- Error message slides down with grid-template-rows (not height), 220ms
- `@media (prefers-reduced-motion: reduce)` fallback for every transition
## Pitfalls
- **Asking for "more animation".** Animate is not a dial. It adds where motion communicates, not everywhere.
- **Removing the reduced-motion fallbacks.** The skill adds them automatically. Non-negotiable for accessibility.
-99
View File
@@ -1,99 +0,0 @@
---
tagline: "Five-dimension technical quality check with P0 to P3 severity."
---
<div class="docs-viz-hero">
<div class="docs-viz-report">
<div class="docs-viz-report-head">
<div>
<div class="docs-viz-report-title">/impeccable audit the checkout flow</div>
<div class="docs-viz-report-target">src/checkout/**</div>
</div>
<div class="docs-viz-report-score">
<span class="docs-viz-report-score-num">2.6</span>
<span class="docs-viz-report-score-out">/ 4</span>
</div>
</div>
<div class="docs-viz-report-dims">
<div class="docs-viz-report-dim">
<span class="docs-viz-report-dim-name">Accessibility</span>
<span class="docs-viz-report-dim-bar"><span class="docs-viz-report-dim-fill docs-viz-report-dim-fill--fail" style="width:50%"></span></span>
<span class="docs-viz-report-dim-score">2 / 4</span>
</div>
<div class="docs-viz-report-dim">
<span class="docs-viz-report-dim-name">Performance</span>
<span class="docs-viz-report-dim-bar"><span class="docs-viz-report-dim-fill" style="width:75%"></span></span>
<span class="docs-viz-report-dim-score">3 / 4</span>
</div>
<div class="docs-viz-report-dim">
<span class="docs-viz-report-dim-name">Theming</span>
<span class="docs-viz-report-dim-bar"><span class="docs-viz-report-dim-fill docs-viz-report-dim-fill--warn" style="width:62%"></span></span>
<span class="docs-viz-report-dim-score">2.5 / 4</span>
</div>
<div class="docs-viz-report-dim">
<span class="docs-viz-report-dim-name">Responsive</span>
<span class="docs-viz-report-dim-bar"><span class="docs-viz-report-dim-fill" style="width:75%"></span></span>
<span class="docs-viz-report-dim-score">3 / 4</span>
</div>
<div class="docs-viz-report-dim">
<span class="docs-viz-report-dim-name">Anti-patterns</span>
<span class="docs-viz-report-dim-bar"><span class="docs-viz-report-dim-fill docs-viz-report-dim-fill--warn" style="width:70%"></span></span>
<span class="docs-viz-report-dim-score">2.8 / 4</span>
</div>
</div>
<div class="docs-viz-report-issues">
<span class="docs-viz-report-sev docs-viz-report-sev--p0">P0<span class="docs-viz-report-sev-n">2</span></span>
<span class="docs-viz-report-sev docs-viz-report-sev--p1">P1<span class="docs-viz-report-sev-n">5</span></span>
<span class="docs-viz-report-sev docs-viz-report-sev--p2">P2<span class="docs-viz-report-sev-n">8</span></span>
<span class="docs-viz-report-sev docs-viz-report-sev--p3">P3<span class="docs-viz-report-sev-n">14</span></span>
</div>
</div>
<p class="docs-viz-caption">Five dimensions scored 0 to 4, each finding tagged P0 (blocks release) to P3 (polish). Audit documents; it doesn't fix. Route the findings into <code>/impeccable harden</code>, <code>/impeccable polish</code>, or <code>/impeccable optimize</code>.</p>
</div>
## When to use it
`/impeccable audit` is the technical counterpart to `/impeccable critique`. Where `/impeccable critique` asks "does this feel right", `/impeccable audit` asks "does this hold up". It runs accessibility, performance, theming, responsive design, and anti-pattern checks against the implementation, scores each dimension 0 to 4, and produces a plan with P0 to P3 severity ratings.
Use it before shipping, during a quality sprint, or whenever a tech lead says "we should really look at accessibility".
## How it works
The skill scans your code across five dimensions:
1. **Accessibility**: WCAG contrast, ARIA, keyboard nav, semantic HTML, form labels.
2. **Performance**: layout thrashing, expensive animations, missing lazy loading, bundle weight.
3. **Theming**: hard-coded colors, dark mode coverage, token consistency.
4. **Responsive**: breakpoint behavior, touch targets, mobile viewport handling.
5. **Anti-patterns**: the same deterministic 25 checks the detector runs.
Each dimension gets a 0 to 4 score. Each finding gets a severity: P0 blocks the release, P1 should fix this sprint, P2 is next cycle, P3 is polish. You get back a single document you can paste into a ticket tracker.
Audit does not fix anything. It documents. Route the findings to `/impeccable polish`, `/impeccable harden`, or `/impeccable optimize` depending on the category.
## Try it
```
/impeccable audit the checkout flow
```
Expected output:
```
Accessibility: 2/4 (partial)
P0: Missing form labels on 4 inputs
P1: Contrast 3.1:1 on disabled button state
P2: No visible focus indicator on custom dropdown
Performance: 3/4 (good)
P1: Hero image not lazy-loaded (340KB)
...
```
Hand the P0s to `/impeccable harden`, the theming and typography P1s to `/impeccable typeset` and `/impeccable polish`, the rest to `/impeccable polish`.
## Pitfalls
- **Confusing it with `/impeccable critique`.** Audit is implementation quality. Critique is design quality. Run both for a full picture.
- **Fixing P3s before P0s.** The severity scale exists for a reason. Start at the top.
- **Skipping the dimensions you think are fine.** Theming and responsive are the ones most people assume are fine until they are not.
-40
View File
@@ -1,40 +0,0 @@
---
tagline: "Push safe designs toward impact without sliding into chaos."
---
## When to use it
Reach for `/impeccable bolder` when the interface looks like every other interface. Generic sans, medium weights, soft shadows, modest accent color, reasonable spacing, forgettable. The design is not wrong, it is just safe. Use bolder when a project can handle presence and the current state is not bringing any.
Do not use it on dashboards people stare at for hours. Boldness earns its place on marketing pages, hero moments, and content features. Not in operator tools.
## How it works
The skill amplifies four axes without breaking usability:
1. **Scale**: display type gets pushed to clamp(3rem, 6vw, 6rem) or beyond. Headlines that fill the viewport, not hedge it.
2. **Weight contrast**: light 300 against heavy 800 instead of medium against regular. Real tension, not a shrug.
3. **Color commitment**: the accent color shows up at full strength, not diluted. Backgrounds can take a stance (ink, accent, cream) instead of all-paper.
4. **Compositional confidence**: asymmetry, off-grid, pullquotes, hanging punctuation, scale jumps. The layout has a voice.
The skill does not add more. It amplifies what is already there. If the design has three colors, bolder does not add a fourth, it commits harder to the three.
## Try it
```
/impeccable bolder the landing page hero
```
Expected changes:
- Hero heading from 3rem to clamp(3.5rem, 7vw, 6.5rem), display font, weight 700
- Subhead from regular to italic at 1.5rem, pulled 8px left of the heading for optical alignment
- Background switches from paper to a cream-to-paper gradient, creating a warmer container
- CTA button fills, drops shadow removed, border radius reduced, hover state inverts colors
- Supporting image pushed slightly off-grid with a negative top margin, creating asymmetry
## Pitfalls
- **Running it on the wrong page.** Product dashboards, settings, and forms should not be bold. They should be legible. Use `/impeccable layout` or `/impeccable polish` instead.
- **Confusing bold with loud.** Bold means committed and confident. Loud means shouting. Bolder is the former. If the result feels aggressive, follow up with `/impeccable quieter`.
- **Pairing it with `/impeccable delight` in the same pass.** Delight works best against a stable visual baseline. Bold first, stabilize, then delight.
-42
View File
@@ -1,42 +0,0 @@
---
tagline: "Rewrite confusing UX copy so interfaces explain themselves."
---
## When to use it
`/impeccable clarify` is for interface text that makes people stop and think. Confusing labels, ambiguous button copy, error messages that blame the user, tooltips that repeat the label, empty states that say nothing useful. Use it when the problem is not the layout or the color, it is the words.
Good triggers: "users do not understand this field", "the error message is not helpful", "I cannot write good button copy", "this tooltip is a waste".
## How it works
The skill rewrites text across the surfaces where most UX copy problems live:
1. **Labels and field hints**: direct, specific, say what is expected.
2. **Button copy**: verb-first, describes the outcome, not the action. "Save changes" not "OK".
3. **Error messages**: explain what went wrong, whose fault it is, and what to do next. Never blame the user.
4. **Empty states**: orient the user, explain why the state is empty, offer a next step.
5. **Tooltips and helper text**: add information the label cannot carry, never restate it.
6. **Confirmation dialogs**: name the consequences, not the action.
The skill uses the audience and mental state from `PRODUCT.md` to tune voice. Technical audience gets precise language. Consumer audience gets plain speech. Rushed users get short text. Anxious users (payment, delete) get reassurance.
## Try it
```
/impeccable clarify the billing form
```
Before and after, typical:
- Label "Billing address" → "Address on your card"
- Placeholder "Enter your VAT ID" → "VAT ID (optional, for business)"
- Error "Invalid input" → "This card number is 15 digits. You entered 14."
- Button "Submit" → "Charge $29 and subscribe"
- Empty state "No transactions yet" → "Your first charge will show up here after your first order."
## Pitfalls
- **Writing cleverer, not clearer.** Clarify is not for voice upgrades. If the copy is already clear, do not reach for this skill. Use `/impeccable delight` instead when you want personality.
- **Skipping the audience question.** Clarify needs to know who is reading. If `PRODUCT.md` does not specify audience technical level, the rewrites will be generic.
- **Running clarify on marketing copy.** Clarify is for functional UX text: labels, errors, instructions. Marketing copy needs a different set of moves and a human writer.
-38
View File
@@ -1,38 +0,0 @@
---
tagline: "Add strategic color to monochrome interfaces without going garish."
---
## When to use it
`/impeccable colorize` is the counterweight to "everything is gray". Dashboards that read as a beige wall, forms with no accent, content pages that could be any SaaS product. Reach for it when the interface is functional but emotionally flat, and you want warmth without tipping into the AI color palette (purple-to-pink, cyan neon, dark mode glow).
## How it works
The skill starts by reading your brand color if one exists, then decides where color earns its place:
1. **Primary action** gets the strongest expression of the brand hue.
2. **Secondary accents** get muted or tinted variants, not a second full color.
3. **Neutrals** get tinted toward the brand hue at low chroma (around 0.005 to 0.01), which is nearly invisible per pixel but creates subconscious cohesion.
4. **Content categories** get a limited, intentional accent system, not a rainbow.
Importantly, it uses OKLCH rather than HSL so that equal lightness steps look equal. As lightness moves toward the extremes, chroma drops automatically. This is how you get color that feels considered instead of computed.
## Try it
```
/impeccable colorize the dashboard
```
Expected diff:
- Brand color moved from a hardcoded hex to `--color-accent: oklch(62% 0.18 240)`
- Neutrals tinted with 0.007 chroma toward the brand hue
- Primary button gets the full accent, secondary buttons get ink/mist
- Chart series uses 3 distinct hues, all at matched lightness so no series visually dominates
- Empty state illustration picks up a soft accent wash
## Pitfalls
- **Running it without a brand hue.** Colorize needs a starting point. If `PRODUCT.md` does not specify one, it will ask. Do not let it pick from the AI color palette defaults.
- **Expecting it to fix the AI color palette problem.** If your design already has purple gradients and cyan neon, you need `/impeccable quieter` first, then colorize can rebuild.
- **Using it on already-colorful interfaces.** That is a `/impeccable quieter` job. Colorize adds, it does not subtract.
-68
View File
@@ -1,68 +0,0 @@
---
tagline: "Shape the design, then build it, all in one flow."
---
<div class="docs-viz-hero">
<div class="docs-viz-flow">
<div class="docs-viz-flow-step">
<span class="docs-viz-flow-num">01</span>
<span class="docs-viz-flow-name">Shape</span>
<span class="docs-viz-flow-hint">Discovery interview. Purpose, users, constraints, direction.</span>
</div>
<div class="docs-viz-flow-step">
<span class="docs-viz-flow-num">02</span>
<span class="docs-viz-flow-name">Load references</span>
<span class="docs-viz-flow-hint">Spatial, typography, motion, color, interaction.</span>
</div>
<div class="docs-viz-flow-step">
<span class="docs-viz-flow-num">03</span>
<span class="docs-viz-flow-name">Build</span>
<span class="docs-viz-flow-hint">Structure, hierarchy, type, color, states, motion, responsive.</span>
</div>
<div class="docs-viz-flow-step docs-viz-flow-step--accent">
<span class="docs-viz-flow-num">04</span>
<span class="docs-viz-flow-name">Iterate visually</span>
<span class="docs-viz-flow-hint">Check in browser, refine until it matches the brief.</span>
</div>
</div>
<p class="docs-viz-caption">Every phase is non-skippable. The discovery step is where most AI output fails: by the time code exists, the thinking is locked in.</p>
</div>
## When to use it
`/impeccable craft` is the end-to-end build command. Give it a feature description and it runs the whole pipeline: structured discovery, reference loading, implementation, visual iteration. Use it when you are starting a new feature from zero and want the whole workflow in one invocation.
Reach for it when:
- **You are building a new feature and want the full flow.** You do not want to manage the steps yourself.
- **You know what you are building but not how it should look.** The discovery phase forces the design thinking before implementation locks it in.
- **You want visual iteration by default.** `craft` checks the result in a browser and refines until the polish is high, instead of shipping the first working version.
If you only want the thinking without the code, use `/impeccable shape` standalone. If you already have a clear vision and just want to build, call `/impeccable` directly with your feature description. `craft` sits in between: structured, complete, opinionated.
## How it works
`craft` runs four phases in order:
1. **Shape the design.** Runs `/impeccable shape` internally: a short discovery conversation about purpose, users, content, constraints, and goals. The output is a design brief you can read and push back on.
2. **Load references.** Based on the brief, pulls in the right reference files (spatial, typography, motion, color, interaction, responsive, UX writing) so the model has the relevant principles loaded before it starts coding.
3. **Build.** Implements the feature in a deliberate order: structure first, then spacing and hierarchy, then type and color, then states, then motion, then responsive. Every decision traces back to the brief.
4. **Visual iteration.** Opens the result in a browser, checks it against the brief and the anti-pattern catalog, and refines until it matches the intent. This step is critical. The first working version is never the shipped version.
The discovery phase is non-skippable and that is the point. Most AI-generated UIs fail because nobody asked what the user was trying to accomplish before the model started writing JSX. `craft` inverts that.
## Try it
```
/impeccable craft a pricing page for a developer tool
```
Expect a 5 to 10 question discovery interview first. Questions about your audience, the product's personality, the emotional tone you want, anti-references, and constraints. Then a design brief. Then implementation, with the browser checked at each stage. Expect multiple iteration rounds in the visual polish phase.
The whole run is longer than a typical command because it includes the thinking, the building, and the refining. That is the trade: more upfront structure, less cleanup afterwards.
## Pitfalls
- **Using it for small changes.** `craft` is for new features, not touch-ups. For existing code, reach for `/impeccable polish`, `/impeccable critique`, or a specific refinement command instead.
- **Rushing the discovery phase.** The interview feels slow compared to "just start coding". It is not. Answering the questions carefully produces a sharper brief, which produces a sharper build, which produces fewer rewrites.
- **Skipping the visual iteration.** The phase exists for a reason. The gap between "technically works" and "feels right" is closed with visual polish, not code review. Let it run.
-109
View File
@@ -1,109 +0,0 @@
---
tagline: "A design review with scoring, persona tests, and automated detection."
---
<div class="docs-viz-hero">
<div class="docs-viz-critique">
<div class="docs-viz-critique-head">
<div class="docs-viz-critique-verdict">
<span class="docs-viz-critique-verdict-label">AI slop verdict</span>
<span class="docs-viz-critique-verdict-value">FAIL</span>
</div>
<span class="docs-viz-report-target">gradient-text &middot; ai-color-palette &middot; nested-cards</span>
</div>
<div class="docs-viz-critique-cols">
<div>
<div class="docs-viz-critique-col-title">Heuristics (Nielsen)</div>
<div class="docs-viz-critique-heuristics">
<div class="docs-viz-critique-heur">
<span>Visibility of status</span>
<span class="docs-viz-critique-heur-score docs-viz-critique-heur-score--good">3</span>
</div>
<div class="docs-viz-critique-heur">
<span>Match with real world</span>
<span class="docs-viz-critique-heur-score docs-viz-critique-heur-score--ok">2</span>
</div>
<div class="docs-viz-critique-heur">
<span>Consistency & standards</span>
<span class="docs-viz-critique-heur-score docs-viz-critique-heur-score--ok">2</span>
</div>
<div class="docs-viz-critique-heur">
<span>Error prevention</span>
<span class="docs-viz-critique-heur-score docs-viz-critique-heur-score--good">3</span>
</div>
<div class="docs-viz-critique-heur">
<span>Recognition over recall</span>
<span class="docs-viz-critique-heur-score docs-viz-critique-heur-score--bad">1</span>
</div>
</div>
</div>
<div>
<div class="docs-viz-critique-col-title">Personas</div>
<div class="docs-viz-critique-personas">
<div class="docs-viz-critique-persona">
<div>
<span class="docs-viz-critique-persona-name">The evaluator</span>
<span class="docs-viz-critique-persona-note">Comparing us to two alternatives on a Tuesday evening.</span>
</div>
<span class="docs-viz-critique-persona-score">2 / 4</span>
</div>
<div class="docs-viz-critique-persona">
<div>
<span class="docs-viz-critique-persona-name">The returning user</span>
<span class="docs-viz-critique-persona-note">Knows the product, on mobile, in a hurry.</span>
</div>
<span class="docs-viz-critique-persona-score">3 / 4</span>
</div>
<div class="docs-viz-critique-persona">
<div>
<span class="docs-viz-critique-persona-name">The skeptic</span>
<span class="docs-viz-critique-persona-note">Has seen every SaaS landing and is bored.</span>
</div>
<span class="docs-viz-critique-persona-score">1 / 4</span>
</div>
</div>
</div>
</div>
</div>
<p class="docs-viz-caption">The two passes (LLM design review plus the deterministic detector) merge into one prioritized list. What's working, what to fix, and the provocative questions worth answering before shipping.</p>
</div>
## When to use it
Reach for `/impeccable critique` when you want an honest second opinion on something you already built. Not "does it work" but "is it any good". The skill scores your interface against Nielsen's 10 heuristics, runs cognitive load checks, tests through persona lenses, and cross-references an automated detector for 25 concrete anti-patterns.
Use it when a page is functionally done and you want to know if it reads as intentional or as AI slop.
## How it works
`/impeccable critique` runs two independent assessments in parallel so they do not bias each other.
The first is an **LLM design review**: the model reads your source, visually inspects the live page if browser automation is available, and walks the impeccable skill's full DO/DON'T catalog. It scores Nielsen's heuristics, counts cognitive load failures, traces the emotional journey through the flow, and flags AI slop.
The second is an **automated detector** (`npx impeccable detect`) that deterministically finds gradient text, purple palettes, side-tab borders, nested cards, line length problems, and the other visible fingerprints of generic AI output.
The two reports merge into one prioritized list: what is working, the three to five things that need fixing, and the provocative questions worth answering before shipping.
## Try it
Point it at a page:
```
/impeccable critique the homepage hero
```
You get back a scored report. Typical shape:
- **AI slop verdict**: pass / fail with the specific tells
- **Heuristic scores**: 10 numbers, 0 to 4
- **Cognitive load**: failure count out of 8
- **Priority issues**: three to five items, each with what, why, and fix
- **Questions to answer**: the ones the interface itself cannot decide for you
From there, pair with `/impeccable polish` or `/impeccable distill` to act on the fixes.
## Pitfalls
- **Running it on incomplete work.** Critique is for finished pages. An empty state with three TODOs will score badly because it is not done, not because it is bad.
- **Ignoring the questions at the end.** They are usually the fixes that change the design most.
- **Treating the heuristic scores as a grade.** They are diagnostic, not evaluative. A 3/4 on a heuristic that matters less for your context is fine.
-42
View File
@@ -1,42 +0,0 @@
---
tagline: "Small moments of personality that turn functional into memorable."
---
## When to use it
`/impeccable delight` is for interfaces that work but do not feel like anything. Use it when the core experience is solid and you want to add the small human touches that make people remember it: a considered empty state, a loading message with a point of view, a success animation that feels earned, a microcopy moment that makes someone smile.
It is a finishing skill. Never the first thing you run on a new build.
## How it works
The skill hunts for delight opportunities in the places most designers skip:
1. **Empty states**: instead of "No items yet", something with personality appropriate to the brand.
2. **Loading and waiting moments**: the best products turn waits into content.
3. **Success feedback**: a moment of celebration when something worth celebrating happens.
4. **Microcopy**: button labels, tooltips, error messages, placeholder text. Tiny copy with taste.
5. **Easter eggs and secondary states**: things users discover that reward paying attention.
The skill reads the brand tone from `PRODUCT.md`. A serious analytics tool gets serious delight (dry, precise, a little clever). A playful consumer app gets more overt personality. It does not force humor where humor is wrong for the audience.
The rule is: every delight moment must still work perfectly if you delete the delight. Nothing depends on the smile.
## Try it
```
/impeccable delight the first-run experience
```
Expected additions:
- Empty dashboard replaces "No data yet" with "Your dashboard is quiet. Let's fix that." plus a single-action CTA.
- Initial sync gets a 3-state loading message that advances: "Finding your accounts... / Pulling the last 30 days... / Making it look good...".
- First successful action triggers a one-time toast with a tiny celebratory moment. After that, just a quiet checkmark.
- Help tooltip on the tricky field has a voice that sounds like a person wrote it.
## Pitfalls
- **Forcing humor.** Not every brand is playful. If the brand voice in `PRODUCT.md` is "clinical and precise", delight adds clever restraint, not jokes.
- **Over-decorating.** One moment of delight is memorable. Twenty becomes noise. The skill is conservative on purpose.
- **Running delight before polish.** Polish fixes what is wrong. Delight adds what is missing. In that order.
-44
View File
@@ -1,44 +0,0 @@
---
tagline: "Ruthless subtraction. Strip designs to their essence."
---
## When to use it
`/impeccable distill` removes what should not be there. Competing buttons, redundant information, decorative borders, three fonts where one works, six navigation items where three belong. Use it when an interface feels cluttered, busy, or like it is trying to do too much at once.
Reach for it after `/impeccable critique` flags "cognitive load" or "visual noise", or any time a page has grown by accretion and no one has done the editing.
## How it works
The skill starts from one question: what is the single job this interface is trying to do? Everything that does not help that job is on the chopping block.
It works in two passes:
1. **Assess the complexity sources**. Too many elements, excessive variation, information overload, visual noise, confusing hierarchy, feature creep. Name each one.
2. **Edit ruthlessly**. Remove what is not essential. Combine what can be combined. Hide what can wait. Consolidate variation into a single treatment. Commit to a single visual language.
The principle: every element on the page has to justify its existence. Fewer obstacles, not fewer features.
## Try it
```
/impeccable distill this dashboard
```
Before: four card styles, three button variants, two header treatments, a sidebar with 14 items grouped into 5 sections.
After a `/distill` pass, typical changes:
- Collapse the four card styles into one
- Pick one button variant, demote the others to text links
- Unify the headers
- Group the sidebar into 3 sections, not 5
- Hide advanced options behind a disclosure
Fewer things. Each one clearer.
## Pitfalls
- **Confusing distill with delete.** Distill removes obstacles. It does not remove features users need. If a user relies on something daily, find a way to keep it quietly, not a way to cut it.
- **Running it too early.** If the feature is still growing, distilling it now means distilling the same thing again next week. Wait until the shape is stable.
- **Expecting it to replace hierarchy work.** Sometimes the right fix is not removing things, it is arranging them. Reach for `/impeccable layout` when the problem is layout, not quantity.
-114
View File
@@ -1,114 +0,0 @@
---
tagline: "Generate a spec-compliant DESIGN.md that captures your visual system so every AI agent stays on-brand."
---
<div class="docs-viz-hero">
<div class="docs-viz-file">
<div class="docs-viz-file-header">
<span class="docs-viz-file-name">DESIGN.md</span>
<span class="docs-viz-file-status">Google Stitch format</span>
</div>
<div class="docs-viz-designmd-section">
<div class="docs-viz-designmd-head">
<span class="docs-viz-designmd-num">01</span>
<span class="docs-viz-designmd-title">Overview</span>
</div>
<p class="docs-viz-designmd-note">Creative North Star: <em>"The Editorial Sanctuary."</em> Quiet type, generous air, one committed accent.</p>
</div>
<div class="docs-viz-designmd-section">
<div class="docs-viz-designmd-head">
<span class="docs-viz-designmd-num">02</span>
<span class="docs-viz-designmd-title">Colors</span>
</div>
<div class="docs-viz-designmd-swatches" aria-hidden="true">
<span class="docs-viz-designmd-swatch" style="background:#1a1a1a"></span>
<span class="docs-viz-designmd-swatch" style="background:#f5f3ef"></span>
<span class="docs-viz-designmd-swatch" style="background:oklch(60% 0.22 30)"></span>
<span class="docs-viz-designmd-swatch" style="background:oklch(90% 0.02 30)"></span>
</div>
</div>
<div class="docs-viz-designmd-section">
<div class="docs-viz-designmd-head">
<span class="docs-viz-designmd-num">03</span>
<span class="docs-viz-designmd-title">Typography</span>
</div>
<div class="docs-viz-designmd-type">
<span class="docs-viz-designmd-type-display">Aa</span>
<span class="docs-viz-designmd-type-body">Cormorant Garamond &middot; Instrument Sans</span>
</div>
</div>
<div class="docs-viz-designmd-section">
<div class="docs-viz-designmd-head">
<span class="docs-viz-designmd-num">04</span>
<span class="docs-viz-designmd-title">Elevation</span>
</div>
<p class="docs-viz-designmd-note">Flat by default. Shadows appear only as a response to state.</p>
</div>
<div class="docs-viz-designmd-section">
<div class="docs-viz-designmd-head">
<span class="docs-viz-designmd-num">05</span>
<span class="docs-viz-designmd-title">Components</span>
</div>
<div class="docs-viz-designmd-comps" aria-hidden="true">
<span class="docs-viz-designmd-btn">Subscribe</span>
<span class="docs-viz-designmd-chip">filter</span>
<span class="docs-viz-designmd-card">card</span>
</div>
</div>
<div class="docs-viz-designmd-section">
<div class="docs-viz-designmd-head">
<span class="docs-viz-designmd-num">06</span>
<span class="docs-viz-designmd-title">Do's and Don'ts</span>
</div>
<div class="docs-viz-designmd-rules">
<span class="docs-viz-designmd-do">Tint neutrals toward the accent hue.</span>
<span class="docs-viz-designmd-dont">Gradient text for emphasis.</span>
</div>
</div>
</div>
<p class="docs-viz-caption">The six sections are fixed, in a fixed order, with fixed names. Alongside, <code>DESIGN.json</code> ships as a machine-readable sidecar for the Live Mode design panel.</p>
</div>
## When to use it
Run `/impeccable document` once you have enough of a visual system to document: colors, typography, at least a button and a card. The command scans your codebase, extracts the tokens and component patterns it finds, and writes a `DESIGN.md` at the project root that follows the [Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/), six sections in a fixed order, interoperable with every other DESIGN.md-aware tool.
Reach for it when:
- **You just ran `/impeccable teach`** and `PRODUCT.md` now exists. Document is the matching visual-side file.
- **A command nudged you toward it.** Live, craft, and polish all read DESIGN.md. If it is missing, the skill suggests running document first.
- **The design has drifted** from an older DESIGN.md and the file no longer describes the live system.
- **Before a large redesign**, to capture current state as a reference for the next direction.
For projects with no code yet (fresh `teach` run, nothing built), there is a seed mode: `/impeccable document --seed` asks five quick strategic questions (color strategy, type direction, motion energy, references, anti-references) and writes a scaffold. Re-run in scan mode once there is code.
## How it works
The scan pass finds design assets in priority order: CSS custom properties, Tailwind config, CSS-in-JS themes, design token files, component source, the global stylesheet, and finally computed styles from the live rendered output if a browser is available. It auto-extracts everything it can, then asks one grouped question for the parts that need creative input: the **Creative North Star** (a single named metaphor for the whole system, like "The Editorial Sanctuary"), descriptive color names, the elevation philosophy, and the component character.
Output is a DESIGN.md with exactly six sections: Overview, Colors, Typography, Elevation, Components, Do's and Don'ts. Headers are fixed character-for-character so the file is parseable by other tools. Alongside it, `DESIGN.json` is written as a machine-readable sidecar. That sidecar is what the live-mode design panel uses to render *this project's* actual button, input, nav, and card tiles instead of a generic approximation.
Every other command reads DESIGN.md on invocation. Variants, polishes, audits, and new features inherit the visual system without being told.
## Try it
```
/impeccable document
```
On a project with tokens already defined, this takes about two minutes: the scan finds your palette and type stack, you pick a North Star from 2 or 3 options, confirm descriptive color names ("Deep Muted Teal-Navy", not "blue-800"), and the file lands at the project root.
On a fresh project:
```
/impeccable document --seed
```
Five questions, about five minutes. The file is a scaffold, marked with a `<!-- SEED -->` comment so it is honest about what it is. Re-run without the flag once you have implemented tokens.
## Pitfalls
- **Running it too early.** On a project with no implemented tokens, seed mode is right. Do not fabricate a full spec the code cannot back up. A fake DESIGN.md is worse than no DESIGN.md.
- **Treating DESIGN.md as documentation for humans only.** It is primarily for the AI. Every other command reads it. The format's forcefulness ("never", "always", Named Rules) is intentional.
- **Adding a Layout / Motion / Responsive top-level section.** The spec has six sections, in a fixed order, with fixed names. Fold layout or motion content into Overview (philosophy-level rules) or Components (per-component behavior).
- **Overwriting an existing DESIGN.md silently.** Document always confirms first. If you want to start fresh, rename the existing file out of the way or explicitly tell the skill to overwrite.
-64
View File
@@ -1,64 +0,0 @@
---
tagline: "Pull reusable components, tokens, and patterns into the design system."
---
<div class="docs-viz-hero">
<div class="docs-viz-flow">
<div class="docs-viz-flow-step">
<span class="docs-viz-flow-num">01</span>
<span class="docs-viz-flow-name">Discover drift</span>
<span class="docs-viz-flow-hint">Repeated hex values, button variants, spacing scales, text styles.</span>
</div>
<div class="docs-viz-flow-step">
<span class="docs-viz-flow-num">02</span>
<span class="docs-viz-flow-name">Propose primitives</span>
<span class="docs-viz-flow-hint">Token names, component APIs with variant + size, text styles.</span>
</div>
<div class="docs-viz-flow-step docs-viz-flow-step--accent">
<span class="docs-viz-flow-num">03</span>
<span class="docs-viz-flow-name">Migrate call sites</span>
<span class="docs-viz-flow-hint">Replace duplicated CSS with the new primitives. No orphan code left behind.</span>
</div>
</div>
<p class="docs-viz-caption">The skill only extracts what's used three or more times with the same intent. Two usages are not a pattern, and migration always happens in the same pass.</p>
</div>
## When to use it
`/impeccable extract` is for the moment your codebase has accidentally become a design system. Repeated button styles in 12 places. Three variants of the same card. Hex colors scattered throughout. Hand-rolled spacing that accidentally matches a scale. Reach for it when you want to consolidate this drift into reusable primitives.
Use it after a product has shipped enough features to reveal the patterns. Premature extraction creates abstractions that do not match reality.
## How it works
The skill discovers the design system structure first, then identifies extraction opportunities:
1. **Tokens**: find repeated literal values (colors, spacing, radii, shadows, font sizes). Propose token names, add to the token system, replace usages.
2. **Components**: find UI patterns that repeat with minor variation (buttons, cards, inputs, modals). Extract into a single component with variants, migrate callers.
3. **Composition patterns**: find layout or interaction patterns that repeat (form rows, toolbar groups, empty states). Extract into composition primitives.
4. **Type styles**: find repeated font-size + weight + line-height combinations. Extract into text styles.
5. **Animation patterns**: find repeated easing, duration, or keyframe combinations. Extract into motion tokens.
The skill is cautious. It only extracts things used three or more times, with the same intent. It never extracts "because it might be reused later". Premature abstraction is worse than duplication.
## Try it
```
/impeccable extract the button styles
```
Expected output:
- Found 14 button instances across 8 files
- 4 distinct variants: primary (filled accent), secondary (bordered), ghost (text-only), destructive (filled red)
- All 4 variants use the same size scale (small, default, large)
- Extracted into `<Button variant="primary" size="default">` with token-driven styles
- Migrated 14 call sites, removed ~180 lines of duplicated CSS
- Added 3 missing tokens: `--button-radius`, `--button-padding-y`, `--button-padding-x`
## Pitfalls
- **Extracting too early.** Two usages are not a pattern. Three might be. Wait until the pattern is obvious.
- **Over-generalizing.** The extracted component should match the current use cases closely, not anticipate every possible future one. You can always add variants later.
- **Forgetting the migration.** Extraction without migration leaves the old duplicated code around and creates a third way of doing the same thing. Always migrate in the same pass.
- **Extracting things that differ in intent.** Two buttons that look similar but serve different purposes (primary action vs link styled as button) should probably stay separate.
-44
View File
@@ -1,44 +0,0 @@
---
tagline: "Make interfaces production-ready. Edge cases, i18n, error states, overflow."
---
## When to use it
`/impeccable harden` is for the day your interface meets reality. Real user data is messy: names that are 60 characters long, product titles in German, prices in the billions, 500 errors, offline modes, right-to-left text. Designs that only work with perfect data are not production-ready.
Reach for it before launch, before opening to a new market, or any time a bug report starts with "our user had a really long name and". For first-run flows, empty-state activation, and onboarding design, reach for `/impeccable onboard` instead.
## How it works
The skill works through four dimensions of real-world resilience:
1. **Text and data extremes**. Long text, short text, special characters, emoji, RTL, numbers in the billions, 1000-item lists.
2. **Error scenarios**. Network failures, API 4xx/5xx, validation errors, permission errors, rate limits, concurrent operations.
3. **Internationalization**. Long translations (German is often 30% longer than English), RTL languages, date and number formats, currency symbols, character sets.
4. **Device and context**. Touch targets, offline behavior, slow connections, low-power mode.
For each dimension it identifies the failure mode, then applies the concrete fix: overflow handling, informative error UI, i18n-safe layouts, pluralization, sensible fallbacks.
## Try it
Start with one page and one dimension:
```
/impeccable harden the user profile page for long names
```
Expected output:
- `.user-name` now has `text-overflow: ellipsis` with a tooltip for the full value
- `.bio` switched from fixed height to `max-height` with a "show more" disclosure
- Added an empty state for users with no bio
- Added a skeleton loader for the async avatar fetch
- Tested at name lengths 1, 20, 60, 200 characters
Run it per-page, not all at once. The first run is the biggest; subsequent runs find fewer issues as patterns solidify.
## Pitfalls
- **Waiting for a bug report.** Harden is preventative. If you find yourself fixing the same class of bug twice, run `/impeccable harden` across the feature.
- **Treating error and empty states as an afterthought.** Most hardening work is error and empty state UI. Budget time for it, not just a `catch` block.
- **Skipping i18n because "we are English-only for now".** i18n-safe layouts are still better layouts. Flexible containers, proper text wrapping, generous line-height. None of that hurts English.
-74
View File
@@ -1,74 +0,0 @@
---
tagline: "The design intelligence behind every command."
---
## When to use it
`/impeccable` is the home command. Call it directly when you want freeform design work with the full guidebook loaded, without picking a specialized command. It is the fallback you reach for when none of the 23 specialists (`audit`, `polish`, `critique`, and the rest) map cleanly onto what you are trying to do.
Reach for `/impeccable` directly when:
- **You are not sure which command fits.** Describe what you want in plain English and let the skill pick the right approach.
- **The work spans multiple disciplines.** "Redo this hero section" touches layout, type, color, and motion. One command cannot own that.
- **You want the full design intelligence without constraints.** Every reference file loaded, every anti-pattern checked, no pre-set workflow.
For structured flows, reach for the specialized commands in the sidebar. Run `/impeccable teach` first on any new project to establish PRODUCT.md and DESIGN.md. `/impeccable craft` chains a discovery interview into a full build with live visual iteration. `/impeccable shape` produces a design brief without touching code. `/impeccable live` gives you a browser picker with three variants per element. The evaluation and refinement commands (`audit`, `critique`, `polish`, `typeset`, `layout`, `colorize`, and the rest) each own a specific slice of the work.
## How it works
Most AI-generated UIs fail the same way: generic fonts, purple gradients, card grids on card grids, glassmorphism everywhere. `/impeccable` gives your AI a strong point of view. It loads an opinionated design handbook plus a long list of anti-patterns, then pushes the model to commit to a specific aesthetic direction before writing a single line of code.
Two files at your project root shape everything the skill does:
- **`PRODUCT.md`** carries register (brand vs product), target users, brand personality, anti-references, design principles. Answers "who, what, why".
- **`DESIGN.md`** carries colors, typography, elevation, components, do's and don'ts, in the six-section Google Stitch format. Answers "how it looks".
Every command reads both files before generating. **Register** decides which defaults load. Brand (marketing, landing, portfolio, where design IS the product) and product (app UI, dashboards, tools, where design SERVES the product) have different defaults for type, motion, color, and density. Specifying it once in PRODUCT.md means `/impeccable typeset` will not push editorial-magazine fonts on a dashboard, and will not push product-fluent defaults on a campaign page. See the [brand vs product tutorial](/tutorials/brand-vs-product) for how the two diverge.
On first use in a project, the skill runs the `teach` flow automatically: a short interview that writes PRODUCT.md and then delegates to `/impeccable document` for DESIGN.md. Future commands read the files without asking again.
## Try it
```
/impeccable redo this hero section
```
```
/impeccable build me a pricing page for a developer tool
```
Both prompts are vague on purpose. `/impeccable` will pick a strong aesthetic direction consistent with your register, commit to non-default fonts, avoid the AI color palette, and make the kind of specific choices that a designer would make. No command name to pick first, no step-by-step workflow to follow.
For visual iteration in the browser rather than chat:
```
/impeccable live
```
Pick any element on your running dev server. Drop a comment or stroke. Get three production-quality variants hot-swapped in via HMR. Accept the one you want and it writes back to source.
## Pin commands back as shortcuts
v3.0 consolidated 18 standalone skills into a single `/impeccable` with 23 sub-commands. If you miss the short form of a specific command, pin it back:
```
/impeccable pin critique
```
From now on, `/critique` invokes `/impeccable critique` directly. It writes a lightweight redirect skill that delegates to the parent, so updates to the skill flow through without re-pinning.
Useful pins to try:
- `/impeccable pin polish` for final-pass work
- `/impeccable pin audit` for deterministic a11y/perf checks
- `/impeccable pin live` for the browser iteration flow
- `/impeccable pin critique` for design review
To remove: `/impeccable unpin critique`. Pins live as directories prefixed with `i-` in your harness skills folder (`.claude/skills/i-critique/`, `.cursor/skills/i-critique/`, etc.), so you can also delete them manually.
## Pitfalls
- **Treating it like a style guide.** It is an opinionated design partner, not a linter. The defaults exist to raise the floor, not to overrule your judgment. If you have a real reason to push back (brand guideline, accessibility constraint, user research), push back and explain why. The skill will work with you. What produces worse output is ignoring the opinion without a reason.
- **Expecting it to fix existing code.** `/impeccable` is for creation. For refinement, reach for `/impeccable polish`, `/impeccable distill`, or `/impeccable critique` instead.
- **Running it before `teach` has had a chance to save context.** On a fresh project it will interview you mid-flight, which is fine but slower. Running `/impeccable teach` explicitly as your very first command is a tiny bit smoother.
- **Skipping the register question.** Brand and product defaults diverge enough that running on the wrong register produces subtly off output. If `PRODUCT.md` has no `## Register` field (legacy), run `/impeccable teach` to add it.
-41
View File
@@ -1,41 +0,0 @@
---
tagline: "Fix layout, spacing, and visual rhythm."
---
## When to use it
`/impeccable layout` is for pages where nothing is technically wrong but nothing is breathing either. Equal padding everywhere, monotonous card grids, content that runs edge to edge, hierarchy that relies on size alone. Reach for it when a layout "feels off" and you cannot articulate why.
Good triggers: "everything feels crowded", "it reads like a wall", "I do not know where to look first".
## How it works
The skill runs through five layout dimensions:
1. **Spacing**: is the spacing scale consistent or are there random 13px gaps, are related elements grouped tightly with generous space between groups, is there any rhythm at all.
2. **Visual hierarchy**: does the eye land on the primary action within 2 seconds, is the hierarchy doing real work or is everything shouting.
3. **Grid and structure**: is there an underlying grid or is the layout random, are elements aligned to baselines.
4. **Rhythm**: does the page alternate between tight and generous spacing, or is everything uniform.
5. **Density**: is the layout cramped or is it wasteful, does density match the content type.
Fixes usually involve rebuilding the spacing scale, introducing asymmetry, collapsing monotonous grids into a mixed layout with hero and supporting elements, and giving the primary action real space.
## Try it
```
/impeccable layout the settings page
```
Typical changes:
- Spacing scale unified to 8 / 16 / 24 / 48 / 96px
- Section breaks at 48px, row gaps at 16px, form field groups at 8px
- Primary actions pulled out of the form flow with 32px buffer
- Decorative borders removed, replaced with spacing-driven grouping
- Sidebar and main column proportions rebalanced (280 / flex vs 25 / 75)
## Pitfalls
- **Confusing arrange with distill.** If the problem is too many things, run `/impeccable distill` first. Layout is for arranging what is already the right set.
- **Expecting it to rescue a broken grid.** If the page has no grid at all, arrange will build one. Just know that the diff is going to be larger than you expect.
- **Ignoring the hierarchy verdict.** If arrange says "nothing is primary", no amount of spacing work fixes that. You need a content decision, not a layout tweak.
-86
View File
@@ -1,86 +0,0 @@
---
tagline: "Iterate on UI in the browser. Pick an element, drop a comment, get three variants. Accept one and it writes to source."
---
<div class="docs-live-callout">
<span class="docs-live-callout-icon" aria-hidden="true">▸</span>
<span class="docs-live-callout-text">See it in action, with the animated demo, at <a href="/live-mode">/live-mode</a>. This page is the reference for what your AI harness reads when the command runs.</span>
</div>
<div class="docs-live-callout">
<span class="docs-live-callout-icon" aria-hidden="true">▸</span>
<span class="docs-live-callout-text"><strong>Status: alpha.</strong> Live Mode works end-to-end and is ready to try, but it still needs more testing against real-world repos and framework configs. Expect rough edges on uncommon setups, and please report what breaks.</span>
</div>
<div class="docs-viz-hero docs-viz-hero--plain">
<div class="docs-viz-live-frame">
<div class="docs-viz-live-chrome">
<span class="docs-viz-live-dot"></span>
<span class="docs-viz-live-dot"></span>
<span class="docs-viz-live-dot"></span>
<span class="docs-viz-live-url">localhost:3000</span>
</div>
<div class="docs-viz-live-stage docs-viz-live-stage--tall">
<div class="docs-viz-live-target">
<span class="docs-viz-live-kicker">No. 04</span>
<h3 class="docs-viz-live-title">Letters, <em>occasionally</em>.</h3>
<p class="docs-viz-live-body">A postcard from the editor, about once a month. No tracking pixels, no "just checking in."</p>
<button class="docs-viz-live-btn" type="button">Send me one</button>
</div>
<div class="docs-viz-live-outline" aria-hidden="true"></div>
<div class="docs-viz-live-ctx" aria-hidden="true">
<button class="docs-viz-live-ctx-nav" type="button" aria-label="Previous"></button>
<span class="docs-viz-live-ctx-counter">2 / 3</span>
<button class="docs-viz-live-ctx-nav" type="button" aria-label="Next"></button>
<span class="docs-viz-live-ctx-divider"></span>
<button class="docs-viz-live-ctx-accept" type="button">Accept</button>
</div>
<div class="docs-viz-live-gbar" aria-hidden="true">
<span class="docs-viz-live-gbar-brand">/</span>
<span class="docs-viz-live-gbar-btn is-active">Pick</span>
<span class="docs-viz-live-gbar-divider"></span>
<span class="docs-viz-live-gbar-x">✕</span>
</div>
</div>
</div>
<p class="docs-viz-caption">Live Mode mid-cycle: the picker outlines the element you chose, the context bar shows which variant you're on, and the global bar stays pinned to the bottom. Accept on this one writes Variant 2 back to source.</p>
</div>
## When to use it
Reach for `/impeccable live` when you want to iterate on something visually the way you would in a design tool, but keep production code as the output. The canvas-like flow of Figma without the round trip to an implementation step.
Use it for:
- **Exploring directions on a real element.** A hero section, a newsletter card, a pricing tier. Three genuinely different takes, side by side, on the actual page with the actual context.
- **Polishing a piece of UI that is almost right.** You know what feels off but cannot quite say it. Pick the element, scribble "more playful" or draw a stroke through the bit that bugs you, hit Go.
- **A quick A/B between two directions your team is debating.** Generate variants, accept nothing, walk away. The point was the comparison.
It is NOT for new greenfield features (reach for `/impeccable craft`) or whole-page redesigns (reach for `/impeccable` or a specialized refine command).
## How it works
One command brings up a picker overlay on top of your running dev server. You pick any element. A small context bar appears next to it. Type a freeform description or pick one of the action chips (`bolder`, `quieter`, `distill`, `polish`, `typeset`, `colorize`, `layout`, `animate`, `delight`, `overdrive`). Optionally drop comment pins or draw strokes directly on the element first, and the skill reads those as intent.
Hit Go. Three **production-quality variants** get generated, each anchored to a genuinely different design archetype (not three riffs on color) and hot-swapped into the page via your framework's HMR. Cycle through them with arrow keys. Accept one and the variant is written back to source. Discard all three and the original stays.
It supports Vite, Next.js (including monorepos), SvelteKit, Astro, Nuxt, and plain static HTML. If your dev server has a strict Content Security Policy, the first-run setup detects it and offers a one-time, dev-only patch so the picker can load. `DESIGN.md` wins on visual decisions, `PRODUCT.md` wins on voice: if you have both, variants stay on-brand without being told.
## Try it
```
/impeccable live
```
Open your dev server URL, pick the newsletter signup card, click the `delight` chip, hit Go. You will get three variants that vary across personality dimensions (a stamp-and-postcard feel, a typographic-surprise version, an illustrated-accent one), not three riffs on the same treatment.
Or pick a hero, type "more editorial, less SaaS", hit Go. The three variants anchor to different editorial archetypes (broadsheet masthead, catalog-style spec rows, oversized-glyph poster) rather than three shades of the same idea.
Stop live mode when you are done: say "stop live mode", close the tab, or hit the exit button on the picker bar.
## Pitfalls
- **Running it on a page that is still half-written.** Live variant generation needs context. If the element has placeholder copy, generic Lorem ipsum, or pre-stylesheet default formatting, variants will reflect that. Fill the content first.
- **Expecting it to make macro decisions.** Live mode iterates on a single picked element. For "redo the entire pricing page", reach for `/impeccable` or `/impeccable craft` instead.
- **Ignoring the fallback messages.** If the element lives in a generated file (a compiled template, a build output), the picker says so explicitly and offers to route the accept into true source. Do not force the accept into the generated file: the next build will wipe it.
- **Running it without PRODUCT.md or DESIGN.md when you care about brand fit.** Live will still generate, but the variants will lean toward generic defaults. Run `/impeccable teach` and `/impeccable document` first if the result needs to sound like your product.
-40
View File
@@ -1,40 +0,0 @@
---
tagline: "Design first-run experiences, empty states, and paths to value."
---
## When to use it
`/impeccable onboard` is for the moments that decide whether a new user sticks around: the first screen, the empty state, the setup flow, the product tour, the "what do I do now" gap. Reach for it when activation is weak, when new users drop off before reaching value, or when your product has empty states that say "no items yet" and stop there.
## How it works
The command starts from one question: what is the aha moment, and how fast can a new user get there. Every design decision points at that moment.
It works across the surfaces that shape first impressions:
1. **First-run experience**: the moments immediately after sign-up. Should the user see a tour, a blank canvas, a filled example, or nothing at all. Pick the approach that matches the product.
2. **Empty states**: every zero-data screen gets oriented. Where am I, why is this empty, what do I do next, what will it look like once it is full.
3. **Setup and installation**: required configuration is minimized, defaults are smart, each step explains why it matters.
4. **Progressive disclosure**: advanced features stay out of the way until they are earned.
5. **Activation events**: the moment a user first experiences the core value is instrumented and celebrated, quietly.
The command resists two common failure modes: over-tutorialized onboarding where users click through a carousel before they can touch anything, and zero-onboarding where users are dropped into an empty app and expected to figure it out.
## Try it
```
/impeccable onboard the editor
```
Typical output:
- First-run: replaces empty editor with a filled example document the user can modify. Cancel button discards the example, edit replaces the content with the user's work.
- Empty state on document list: "No documents yet. Create your first, or import from Notion, Google Docs, or Markdown."
- Setup: reduced from 6 required fields to 1 (workspace name). Everything else has a smart default and can be edited later in settings.
- Activation: the first time a user saves a document, a quiet toast says "Saved. Your work is in the cloud now." One-time, not repeated.
## Pitfalls
- **Adding a product tour as the default answer.** Most products do not need a tour. They need a better first screen. Tours are a crutch.
- **Designing onboarding without defining the aha moment.** If you cannot say in one sentence what the user should feel in the first 60 seconds, go back to `/impeccable shape` first.
- **Running onboard on a broken flow.** Fix the flow first. Onboarding cannot rescue a product where the core action is broken.
-56
View File
@@ -1,56 +0,0 @@
---
tagline: "Diagnose and fix UI performance from LCP to bundle size."
---
## When to use it
`/impeccable optimize` is for interfaces that feel slow. First paint takes forever, scrolling janks, images pop in late, interactions feel laggy, the bundle ships 800KB of JavaScript. Use it when the Web Vitals are bad or when users are complaining that things are sluggish.
Do not use it as premature optimization. If LCP is 1.1s and INP is 80ms, stop. The design work matters more.
## How it works
The skill works through five perf dimensions:
1. **Loading and Web Vitals**: LCP, INP, CLS. Identify what is blocking the first paint, what is delaying interaction, what is shifting layout.
2. **Rendering**: unnecessary re-renders, missing memoization, expensive reconciliation, layout thrash in loops.
3. **Animations**: is anything animating layout properties, are transforms and opacity the only thing touched, does `will-change` help or hurt here.
4. **Images and assets**: lazy loading, responsive images (`srcset`, `sizes`), modern formats (WebP, AVIF), dimensions set to prevent CLS.
5. **Bundle size**: unused imports, oversized dependencies, missing code-splitting, dead code.
The skill measures before and after. Every fix gets quantified. If a change does not move a metric, it gets rolled back.
## Try it
```
/impeccable optimize the homepage
```
Expected shape:
```
LCP: 3.2s → 1.4s
- Hero image preloaded (-800ms)
- Removed render-blocking font stylesheet (-240ms)
- Deferred analytics script (-180ms)
INP: 240ms → 90ms
- Debounced scroll handler
- Memoized expensive list render
- Removed synchronous layout read in event loop
CLS: 0.18 → 0.02
- Set dimensions on hero image and logo
- Reserved space for async header badge
Bundle: 340KB → 180KB
- Removed unused lodash import (52KB)
- Code-split the playground route (78KB)
- Dropped deprecated icon set (30KB)
```
## Pitfalls
- **Optimizing before measuring.** Without baseline metrics, you cannot tell what helped. Run `/impeccable optimize` with specific Web Vitals numbers, not vibes.
- **Chasing tiny wins.** A 20ms improvement in INP that takes a week is rarely worth it. Optimize has diminishing returns; know when to stop.
- **Forgetting to re-measure after every change.** The build could have made things worse in a way the skill did not predict. Verify.
-30
View File
@@ -1,30 +0,0 @@
---
tagline: "Push an interface past conventional limits. Shaders, physics, 60fps, cinematic transitions."
---
## When to use it
`/impeccable overdrive` is for the moments where you want to impress. A hero that uses WebGL. A table that handles a million rows. A dialog that morphs out of its trigger element. A form that validates in real-time with streaming feedback. A page transition that feels cinematic. Use it when the project budget allows for technical ambition and the outcome needs to feel extraordinary.
Do not use it on operator tools, dashboards, or anything where reliability beats spectacle. Overdrive burns complexity for effect, and that trade-off is only worth it on moments that matter.
## How it works
The skill picks one moment to make extraordinary and commits to it, rather than spreading effort across the whole interface. It then reaches for techniques most AI-generated UIs never touch: WebGL shaders, spring physics, Scroll Timeline, View Transitions, canvas animation, GPU-accelerated filters. Everything is budgeted, profiled, and tested at 60fps, with reduced-motion fallbacks baked in.
Overdrive output is announced with `──── ⚡ OVERDRIVE ────` so you know you are entering a more ambitious mode. Expect larger diffs, new dependencies, and implementation depth beyond what other skills produce.
## Try it
```
/impeccable overdrive the landing hero
```
One concrete run might replace a static hero with a WebGL shader background driven by mouse position, a display headline that reveals with a mask on scroll using the Scroll Timeline API, and a View Transition on the CTA that morphs into the next page. Plus a reduced-motion fallback that swaps all of it for a clean static composition.
## Pitfalls
- **Using it everywhere.** Overdrive works because it is rare. If every page has cinematic moments, none of them are cinematic.
- **Shipping without reduced-motion fallbacks.** Non-negotiable. Overdrive adds them automatically; do not remove them.
- **Ignoring performance.** Extraordinary moments still need to hit 60fps. If the effect drops frames, cut it or optimize. Slow spectacle is worse than simple done well.
- **Running overdrive before the base interface is solid.** Spectacle on a broken foundation reads as distraction, not delight.
-46
View File
@@ -1,46 +0,0 @@
---
tagline: "The meticulous final pass between good and great."
---
## When to use it
`/impeccable polish` is the last thing you run before shipping. It hunts down the small details that separate a shipped feature from a polished one: half-pixel misalignments, inconsistent spacing, forgotten focus states, loading transitions that flash, copy that drifts in tone. It also aligns the feature with your design system, replacing hard-coded values with tokens, swapping custom components for shared ones, and fixing any drift from established patterns.
Reach for it when the feature is functionally complete, nothing is broken, and something still feels off. Also reach for it when a feature has drifted from the design system and needs to be pulled back in line.
## How it works
Polish starts by discovering the design system (tokens, spacing scale, shared components), then works methodically across six dimensions:
1. **Visual alignment and spacing**: pixel-perfect grid adherence, consistent spacing scale, optical alignment on icons.
2. **Typography**: hierarchy consistency, line length, widows and orphans, kerning on headlines.
3. **Color and contrast**: token usage, theme parity, WCAG ratios, focus indicators.
4. **Interaction states**: hover, focus, active, disabled, loading, error, success. Every state accounted for.
5. **Transitions and motion**: smooth easing, no layout jank, respect for `prefers-reduced-motion`.
6. **Copy**: consistent voice, correct tense, no placeholder strings, no stray TODOs.
The skill is explicit about one thing: polish is the last step, not the first. If the feature is not functionally complete, polishing it is wasted work.
## Try it
```
/impeccable polish the pricing page
```
A healthy run looks like:
```
Visual alignment: fixed 3 off-grid elements (8px baseline)
Typography: tightened h1 kerning, fixed widow on testimonial
Interaction: added hover state on FAQ items, focus ring on email input
Motion: softened modal entrance, added reduced-motion fallback
Copy: removed one "Lorem ipsum" stray, aligned button voice
```
Five small fixes, no rewrites. That is the shape of a good polish pass.
## Pitfalls
- **Polishing work that is not done.** If there are TODOs in the code, you are not ready. Run `/impeccable polish` on finished features only.
- **Treating polish as redesign.** Polish refines what exists. If you find yourself rearchitecting a layout, you needed `/impeccable critique` or `/impeccable layout` instead.
- **Running `/impeccable polish` without `/impeccable audit` first.** Polish catches feel-based issues. Audit catches measurable ones. Use both.
-40
View File
@@ -1,40 +0,0 @@
---
tagline: "Tone down designs that are shouting without losing their intent."
---
## When to use it
`/impeccable quieter` is the counterweight to `/impeccable bolder`. Reach for it when an interface is visually aggressive, overstimulating, or trying to do too many things at full volume. Neon on dark, gradient text everywhere, 6 accent colors, everything animated, 20px shadows. Use quieter when the design needs to breathe and you want refinement without losing the point of view.
Also useful after `/impeccable bolder` goes a little too far.
## How it works
The skill works by reduction across four axes:
1. **Color**: desaturate, lower chroma in OKLCH, pull accents back to a single primary plus muted support. No more than two intentional colors.
2. **Contrast**: soften extreme darks and lights, pull the range in. Backgrounds move from pure white and pure black to paper and ink.
3. **Decoration**: remove shadows that are not doing work, drop borders that are not carrying structure, retire gradients that exist for energy rather than hierarchy.
4. **Motion and effect**: slow animations down, remove anything that auto-plays, drop parallax and blur unless they serve readability.
The skill preserves the design's intent. If the original had a point of view, the quieter version has the same point of view with more confidence. Refinement, not neutralization.
## Try it
```
/impeccable quieter the pricing page
```
Typical diff:
- Gradient text on the price removed, replaced with solid ink at one weight heavier
- Three accent colors reduced to one (magenta), the other two become neutral variants
- Card shadows reduced from `0 20px 40px rgba(0,0,0,0.2)` to `0 1px 0 var(--color-mist)` (a hairline)
- Background switches from dark gradient to paper with a subtle cream wash at the top
- Hero animation from 1.2s easeOut with 3 staggered elements to a single 260ms fade-in
## Pitfalls
- **Over-applying.** Quieter can strip personality if you run it on something that was already measured. Use it when the design is too loud, not when it is correctly assertive.
- **Confusing quieter with distill.** Quieter reduces intensity. Distill removes elements. They are different moves.
- **Running it in response to a critique that says "too busy".** Busy usually means too many things, not too loud. Try `/impeccable distill` first.
-73
View File
@@ -1,73 +0,0 @@
---
tagline: "Think before you build. Produce a design brief through discovery, not guesswork."
---
<div class="docs-viz-hero">
<div class="docs-viz-file">
<div class="docs-viz-file-header">
<span class="docs-viz-file-name">brief.md</span>
<span class="docs-viz-file-status">Output of /impeccable shape</span>
</div>
<div class="docs-viz-file-body">
<div class="docs-viz-file-row">
<span class="docs-viz-file-k">Purpose</span>
<span class="docs-viz-file-v">Let committed subscribers change what they get without losing them to unsubscribe.</span>
</div>
<div class="docs-viz-file-row">
<span class="docs-viz-file-k">User</span>
<span class="docs-viz-file-v">Rushed, on mobile, mid-meeting. Reading fast, low patience.</span>
</div>
<div class="docs-viz-file-row">
<span class="docs-viz-file-k">Content</span>
<span class="docs-viz-file-v">4 digest types, 2 cadences, one opt-out-all at the bottom.</span>
</div>
<div class="docs-viz-file-row">
<span class="docs-viz-file-k">Feeling</span>
<span class="docs-viz-file-v">Calm, trustworthy, no dark patterns.</span>
</div>
<div class="docs-viz-file-row">
<span class="docs-viz-file-k">Constraints</span>
<span class="docs-viz-file-v">Mobile-first. WCAG AA contrast. One column, no modals.</span>
</div>
</div>
<div class="docs-viz-file-footer">Hand it to <code>/impeccable</code>, <code>/impeccable craft</code>, or any implementation flow.</div>
</div>
<p class="docs-viz-caption">A shape brief is a compass, not a spec. It captures intent, not UI. Implementation skills read it before writing a line of code.</p>
</div>
## When to use it
`/impeccable shape` is where a feature starts. Before anyone writes code, before anyone argues about the hero treatment, before anyone picks a font. Use it to force a discovery conversation about purpose, users, content, and constraints, then capture the answers as a design brief the implementation skills can lean on.
Reach for it whenever a feature is about to start, a ticket is vague, or you catch yourself writing JSX to figure out what the product should be.
## How it works
Most AI-generated UIs fail not because of bad code, but because of skipped thinking. The model jumps to "here is a card grid" without asking "what is the user trying to accomplish". `/impeccable shape` inverts that order.
The skill runs a structured discovery interview in conversation. It will not write code during this phase. The questions cover:
- **Purpose and context**: what the feature is for, who uses it, what state of mind they are in
- **Content and data**: what is displayed, realistic ranges, edge cases, what is dynamic
- **Design goals**: the single most important thing, the intended feeling, reference examples
- **Constraints**: technical, content, accessibility, localization
You answer naturally. The skill asks follow-ups, not a form. At the end it produces a design brief: a structured artifact you can hand to `/impeccable` or any other implementation skill.
Note: if you want the full flow (discovery interview, then straight into building), use `/impeccable craft` instead. It runs `/impeccable shape` internally, then continues into implementation with visual iteration. `/impeccable shape` standalone is for when you want just the brief, so you can take it to whatever implementation approach you prefer.
## Try it
```
/impeccable shape a daily digest email preferences page
```
Expect a 5 to 10 question conversation. The skill asks things like "who is the person opening this, and are they already committed or still curious" and "what happens when the user has unsubscribed from everything, do we hide the feature or show something". You answer, and a brief materializes.
From there you can hand the brief to `/impeccable`, `/impeccable polish`, or any other skill. Or just use it as a reference while you build by hand.
## Pitfalls
- **Skipping it because it feels slow.** The interview is maybe 5 minutes. The rewrites you avoid are measured in hours.
- **Treating the brief as a spec.** It is a compass, not a checklist. It captures intent, not UI.
- **Answering with "standard" or "normal".** Specificity is the whole point. If a user is "rushed, on mobile, between meetings", say so. That changes everything downstream.
-70
View File
@@ -1,70 +0,0 @@
---
tagline: "Teach Impeccable who your product is for, once per project."
---
<div class="docs-viz-hero">
<div class="docs-viz-file">
<div class="docs-viz-file-header">
<span class="docs-viz-file-name">PRODUCT.md</span>
<span class="docs-viz-file-status">Loaded on every command</span>
</div>
<div class="docs-viz-file-body">
<div class="docs-viz-file-row">
<span class="docs-viz-file-k">Register</span>
<span class="docs-viz-file-v">Product. Design serves the task.</span>
</div>
<div class="docs-viz-file-row">
<span class="docs-viz-file-k">Users</span>
<span class="docs-viz-file-v">SREs on call, reading fast, often in the dark.</span>
</div>
<div class="docs-viz-file-row">
<span class="docs-viz-file-k">Brand voice</span>
<span class="docs-viz-file-v">Calm, clinical, no hype.</span>
</div>
<div class="docs-viz-file-row">
<span class="docs-viz-file-k">Anti-references</span>
<span class="docs-viz-file-v">Purple gradients. Glassmorphism. "Boost your productivity."</span>
</div>
</div>
<div class="docs-viz-file-footer">Every command reads this before writing a line of code.</div>
</div>
<p class="docs-viz-caption">A finished PRODUCT.md. Strategy only: who, what, why. No colors, no fonts, no pixel values, those live in DESIGN.md.</p>
</div>
## When to use it
Run `/impeccable teach` once at the start of a project. It is the onramp. Without it, every other command will produce design that is technically competent but generically toned: stock SaaS voice, safe-default fonts, the AI color palette. With it, every command reads your answers before it generates.
Reach for it when:
- **You just installed Impeccable in a new project.** First thing to run. Other commands will nudge you toward it if you skip.
- **The project's brand direction has shifted.** New positioning, new audience, new voice. Re-run `teach` and the updated context flows through every command.
- **Another command said "no design context found"** and stopped. That is the signal: run teach, then resume.
## How it works
Teach writes two complementary files at the project root:
- **`PRODUCT.md`** is the strategic file. Register (brand or product), target users, product purpose, brand personality, anti-references, design principles, accessibility needs. Answers "who, what, why".
- **`DESIGN.md`** is the visual file. Colors, typography, elevation, components, do's and don'ts. Answers "how it looks". Written by the delegated `/impeccable document` command, which teach invokes at the end.
The flow scans the codebase first (README, package.json, components, tokens, brand assets) and forms a **register hypothesis**: brand (landing, marketing, portfolio, where design IS the product) or product (app UI, dashboards, tools, where design SERVES the product). Register is the first question, because it shapes every downstream answer: typography defaults, motion energy, color strategy, the reference set commands like `/impeccable typeset` pull from. After register, teach asks only what it could not infer: users, personality in three real words, references and anti-references, accessibility requirements.
PRODUCT.md is strategic only. No colors, no fonts, no pixel values. Those live in DESIGN.md. Keeping the two files separate is deliberate: strategy can stay stable while the visual system evolves.
## Try it
```
/impeccable teach
```
Expect a 5 to 8 minute interview. The first question is usually about register; the rest are short. Teach will quote back what it inferred from your code ("from the routes, this looks like a product surface, match?") so you are confirming, not starting from scratch.
At the end, teach offers to run `/impeccable document` for you. Say yes unless you have a specific reason to hold off. A real DESIGN.md is what keeps variants, polishes, and audits on-brand.
## Pitfalls
- **Skipping it to "just try a command quickly".** Every other command will interview you mid-flight instead. Running teach first is faster, not slower.
- **Giving generic answers.** "Modern and clean" is not useful. "Warm, mechanical, opinionated" is. Be specific. Be willing to disagree with safe defaults.
- **Treating PRODUCT.md as immutable.** The file is yours. If teach put something in there that is not quite right, edit it. Every command reads the current file.
- **Listing only adjectives for references.** Brands, products, printed objects: named, not described. "Klim Type Foundry specimen pages", not "technical and clean". Anti-references should be equally specific.
-42
View File
@@ -1,42 +0,0 @@
---
tagline: "Fix typography that feels generic, inconsistent, or accidental."
---
## When to use it
Reach for `/impeccable typeset` when the text on a page looks like default typography instead of designed typography. Muddy hierarchy, three sizes that look the same, body copy at 14px, a display font that is actually just Inter bold, headlines with no kerning attention.
Common triggers: "hierarchy feels flat", "readability is off", "fonts look generic".
## How it works
The skill assesses typography across five dimensions:
1. **Font choices**: are you using invisible defaults (Inter, Roboto, Arial, Open Sans), does the typeface match the brand, are there more than 2 to 3 families.
2. **Hierarchy**: are heading, body, and caption clearly different at a glance, is the size contrast at least 1.25x between steps, are weight contrasts legible.
3. **Sizing and scale**: is there a coherent type scale, does body text meet 16px minimum, is the scale fixed-rem for app UIs or fluid-clamp for marketing pages.
4. **Readability**: line length 45 to 75 characters, line-height tuned for font and context, contrast.
5. **Consistency**: same element uses same treatment everywhere, no one-off font-size overrides.
It then fixes what it finds: picks distinctive typefaces, builds a modular scale, widens hierarchy contrast, sets proper line length and leading.
## Try it
```
/impeccable typeset the article layout
```
Expected diff:
- Display font swapped from Inter 700 to a real display face
- Type scale rebuilt: 3rem / 2rem / 1.25rem / 1rem / 0.875rem, ratio 1.333
- Body text bumped from 14px to 16px
- Line length clamped to 68ch on the article column
- Line-height 1.6 for body, 1.1 for display
- Removed four one-off `font-size` values scattered in component styles
## Pitfalls
- **Asking for a new font without context.** Typeset will pick based on the `PRODUCT.md` brand voice. If you have not run `/impeccable teach`, the suggestion will be generic.
- **Reaching for typeset when the issue is layout.** If paragraphs are fine but the page feels cramped, you want `/impeccable layout`.
- **Expecting fluid clamp scales on app UIs.** Typeset uses fixed rem scales for app interfaces. Fluid typography is for marketing and content pages where line length varies dramatically.
-130
View File
@@ -1,130 +0,0 @@
---
title: Brand vs product, pick a register
tagline: "Two worlds, two sets of defaults. Pick the right one and every command downstream benefits."
order: 3
description: "Impeccable treats brand work (landing pages, campaigns, portfolios) and product work (app UI, dashboards, tools) as different worlds with different defaults. Learn how to pick a register and how it shapes every command that reads it."
---
## See the divergence
Same element, one register each. A newsletter signup, twice.
<div class="docs-viz-hero docs-viz-hero--plain">
<div class="docs-viz-register">
<div class="docs-viz-register-side">
<div class="docs-viz-register-label">
<span class="docs-viz-register-name">Brand</span>
<span class="docs-viz-register-lane">Editorial-magazine</span>
</div>
<div class="docs-viz-register-frame docs-viz-register-frame--brand">
<span class="docs-viz-reg-kicker">No. 04 &nbsp;·&nbsp; Dispatch</span>
<h3 class="docs-viz-reg-title">Letters, occasionally.</h3>
<p class="docs-viz-reg-body">A postcard from the editor, once a month. No tracking pixels, no "just checking in."</p>
<span class="docs-viz-reg-btn">Send me one</span>
</div>
<div class="docs-viz-register-notes">
<span>Serif display, italic display weight</span>
<span>Drenched in the primary hue</span>
<span>Monospaced kicker, editorial voice</span>
</div>
</div>
<div class="docs-viz-register-side">
<div class="docs-viz-register-label">
<span class="docs-viz-register-name">Product</span>
<span class="docs-viz-register-lane">Utility / app shell</span>
</div>
<div class="docs-viz-register-frame docs-viz-register-frame--product">
<span class="docs-viz-reg-kicker">Newsletter</span>
<h3 class="docs-viz-reg-title">Subscribe to updates</h3>
<p class="docs-viz-reg-body">Product changes and release notes, once a month. Unsubscribe at any time.</p>
<span class="docs-viz-reg-btn">Subscribe</span>
</div>
<div class="docs-viz-register-notes">
<span>Neutral sans, semibold for hierarchy</span>
<span>Restrained palette, accent only on state</span>
<span>Short, scannable, mobile-readable copy</span>
</div>
</div>
</div>
<p class="docs-viz-caption">The table below lists what's different. This is what it looks like at the pixel.</p>
</div>
## Why register matters
Every design task belongs to one of two worlds:
- **Brand** is where design IS the product. Marketing sites, landing pages, portfolios, long-form content, campaign surfaces. Distinctiveness is the bar. Fonts, motion, density, and color all push toward "this looks like nothing else in the category."
- **Product** is where design SERVES the product. App UI, admin, dashboards, tools. Earned familiarity is the bar. Fluent users of Linear, Figma, Notion, Raycast, or Stripe should trust the output on sight.
If you ask the same AI to design a dashboard and a campaign page without naming which world, you'll get the average of the two. Brand surfaces will feel too careful. Product surfaces will feel too precious. Register is how Impeccable avoids that.
Impeccable tracks register as a single field in `PRODUCT.md`:
```markdown
## Register
product
```
That is it: a bare value, `brand` or `product`. Every command that does register-sensitive work (`typeset`, `animate`, `colorize`, `layout`, `bolder`, `quieter`, `delight`) loads a different reference file based on what it finds here.
## How the two worlds diverge
This is not an exhaustive list, the full divergence lives in the `brand.md` and `product.md` reference files, but the shape of the difference:
| Dimension | Brand | Product |
|---|---|---|
| **Type lanes** | Editorial-magazine, luxury, brutalist, consumer-warm, tech-minimal, all available. Swing. | Tighter set: neutral sans + optional mono, sized for dense reading, fluid type reserved for marketing surfaces. |
| **Motion** | Choreographed entrances, scroll-driven sequences, decorative moments earn their place. | Restrained. State changes only. Animation serves feedback, not atmosphere. |
| **Color** | Full palette, Committed, or Drenched are all on the table. | Restrained by default. Accents carry meaning; color is not decoration. |
| **Density** | Whatever the narrative wants. Generous whitespace or packed rule-divided columns both valid. | Comfortable to dense. Every pixel earns its place. |
| **References** | Real-world, from the right lane. *Klim specimen pages* or *Broadsheet masthead*, not "modern SaaS". | Category best-tool. *Linear*, *Figma*, *Notion*, *Raycast*, *Stripe*. |
The same command, `/impeccable typeset`, pulls from different fonts in the two worlds. The same command, `/impeccable animate`, picks different motion vocabularies. The same command, `/impeccable layout`, assumes different density defaults. You do not re-learn the command: you answer the register question once, and the command adapts.
## Step 1. Decide or inherit
If you haven't run `/impeccable teach` yet, run it now. The first question is about register:
```
/impeccable teach
```
Teach scans your codebase first and forms a hypothesis: routes like `/`, `/pricing`, `/blog`, hero sections, scroll-driven content point toward brand. Routes like `/app`, `/dashboard`, `/settings`, forms and tables point toward product. It leads with the hypothesis rather than starting cold:
> From the codebase, this looks like a product surface, does that match your intent, or should we treat it differently?
If the project genuinely spans both (a product with a big marketing landing), teach asks which register describes the **primary** surface. Register is per-project, not per-page, but you can override it per task when needed.
## Step 2. Verify the register landed
Open `PRODUCT.md` and look for the `## Register` section. It should carry a bare value, not prose:
```markdown
## Register
brand
```
If the section is missing (you're on an older `PRODUCT.md` from pre-v3.0), re-run `/impeccable teach`. It will detect the gap and add the field without re-interviewing you on everything else.
## Step 3. Override per task when you need to
Most of the time, register is set once and forgotten. But a product project might occasionally need a single brand surface (a launch landing, an investor one-pager) without flipping the whole project.
You have two options:
- **Name it in the brief.** "`/impeccable craft a launch landing for v2, brand register for this one page.`" The skill honors the override for that task only.
- **Set a per-surface register.** If the override is lasting, add a short note in `PRODUCT.md` under an explicit section: `## Register overrides: /launch is brand.` Commands that read PRODUCT.md will respect it.
## What to try next
- Run a command that is register-sensitive and watch the divergence: `/impeccable typeset the pricing page` on a product project vs. a brand project will pick different type families, different scale ratios, and different pairings.
- Pair with [getting started](/tutorials/getting-started) if you haven't installed Impeccable yet.
- Reach for `/impeccable document` after teach to capture the visual side (colors, components) into DESIGN.md.
## Common issues
- **Register keeps slipping the wrong way.** If you set `product` but commands keep producing brand-feeling output, check that `PRODUCT.md` is at the project root and the `## Register` section has a bare value (no prose, no explanation, just the word). Commands can only read what is there.
- **The hypothesis teach formed is wrong.** Disagree in the answer. Teach is asking, not telling.
- **A project is genuinely 50/50.** Pick the primary surface, then use per-task overrides for the minority one. Trying to average the two in PRODUCT.md produces worse output than committing to one.
@@ -1,129 +0,0 @@
---
title: Critique with the visual overlay
tagline: "Use /impeccable critique plus the browser overlay to review a live page with ground truth."
order: 4
description: "Run a full design critique that combines LLM assessment, the automated detector, and a live browser overlay so you can see exactly which elements trigger which anti-patterns on the page you're looking at."
---
## What you'll build
You will run a complete design critique against a live page in your browser, with every flagged anti-pattern highlighted directly on the element that caused it. No screenshots, no guesswork, no paragraph of findings you have to map back to the code.
Total time: about ten minutes.
## Prerequisites
- Impeccable installed in your project (see [getting started](/tutorials/getting-started) if you have not).
- A harness with browser automation available (Claude Code with the Chrome extension, or similar).
- A page you want to critique, either local (`localhost:3000/pricing`) or deployed.
## Step 1. Run /impeccable critique
From your harness, run:
```
/impeccable critique the pricing page at localhost:3000/pricing
```
The skill kicks off two independent assessments in parallel. They run in separate sub-agents so one does not bias the other.
### What the LLM assessment does
The first assessment reads your source code and, if browser automation is available, opens the live page in a new tab. It walks the full impeccable skill DO/DON'T catalog and scores the page against Nielsen's 10 heuristics, the 8-item cognitive load checklist, and the brand fit from your `PRODUCT.md`.
It labels the tab it opens with `[LLM]` in the title so you can tell which one is which.
### What the automated detector does
The second assessment runs `npx impeccable detect` against the page. This is deterministic: around thirty specific pattern checks that fire or do not fire. Gradient text, purple palettes, side-tab borders, nested cards, line length problems, low contrast, tiny body text, and the rest. The [full catalog](/anti-patterns) lists every rule and which layer (CLI, browser, or LLM-only) catches it.
You get back a JSON list of every finding with its element selector, the rule that fired, and a short description.
## Step 2. Open the visual overlay
Impeccable ships with a visual mode that highlights every detected anti-pattern directly on the page. Here is what it looks like running on a deliberately-bad synthwave landing page:
<div class="tutorial-embed">
<div class="tutorial-embed-header">
<span class="tutorial-embed-dot red"></span>
<span class="tutorial-embed-dot yellow"></span>
<span class="tutorial-embed-dot green"></span>
<span class="tutorial-embed-title">Live detection overlay</span>
</div>
<iframe src="/antipattern-examples/visual-mode-demo.html" class="tutorial-embed-iframe" loading="lazy" title="Impeccable visual overlay running on a demo page"></iframe>
</div>
Every outlined element has a floating label naming the rule that fired. Hover an outline to see the full finding. This is exactly what you will see on your own page.
You have two ways to open it:
1. **[Chrome extension](https://chromewebstore.google.com/detail/impeccable/bdkgmiklpdmaojlpflclinlofgjfpabf)**: one-click activation on any page. Click the Impeccable icon in the toolbar and every anti-pattern gets highlighted instantly.
2. **Inside `/impeccable critique`**: the skill opens a browser tab labeled `[Human]` with the detector active during the browser portion of the assessment. You do not need to do anything extra.
For this tutorial, the easiest option is the Chrome extension. Install it, navigate to your pricing page, and click the Impeccable icon. You will see the overlay appear immediately on the live page.
## Step 3. Merge the two assessments
Back in your harness, `/impeccable critique` has finished and produced a combined report. It looks something like:
```
AI slop verdict: FAIL
Detected tells: gradient-text (2), ai-color-palette (1),
nested-cards (1), side-tab (3)
Heuristic scores (avg 2.8/4):
Visibility of status: 3 (good)
Match between system and real world: 2 (partial)
Consistency and standards: 2 (partial)
...
Cognitive load: 3/8 failures (moderate)
Visible options at primary decision: 6 (flag)
Decision points stacked at top: yes (flag)
Progressive disclosure: absent on advanced pricing toggles
What's working:
- Clear price hierarchy
- Strong headline
Priority issues:
1. Hero uses gradient text on the main price
Why: AI tell, reduces contrast, hurts scannability
Fix: solid ink color at one weight heavier
2. Feature comparison table has 4 nested card levels
Why: visual noise, unclear hierarchy
Fix: flatten to a table with zebra striping
Questions to answer:
- Is the free tier a real product or a funnel?
- What does a user feel when they land here from an ad vs from search?
```
## Step 4. Fix the findings
The report gives you a priority list. You can work through them one at a time, ask the model to fix them all at once, or anything in between. What matters is using the overlay to verify:
1. Keep the overlay open in one tab.
2. Make fixes in code (or ask the model to fix everything).
3. Reload. The overlay re-scans and resolved findings disappear.
This feedback loop is the reason the overlay matters. You see fixes land in real time, and you never ship a "fix" that did not actually satisfy the rule.
## Step 5. Re-run when you are done
After you have worked through the priority list, run `/impeccable critique` again. The goal is a clean AI slop verdict and at least a 3.5 average on the heuristics. Cognitive load should be below 2 failures.
If something still fires, fix it or write a suppression comment explaining why the rule does not apply in your context (the detector respects a small set of opt-out pragmas, but use them sparingly).
## What to try next
- [Iterate on the critique findings with Live Mode](/tutorials/iterate-live). Pick the element critique flagged, drop a comment, get three redirections hot-swapped in place, and write the accepted one back to source.
- `/impeccable audit the same page` to catch the implementation issues critique does not cover (accessibility, performance, theming).
- `/impeccable polish` if the critique report is clean and you want the last-mile refinement pass.
- `/impeccable distill` if critique flagged "too busy" or "cognitive load". Distill removes what should not be there.
## Common issues
- **The overlay shows no findings but critique says there are problems**. The detector catches deterministic patterns. Critique catches judgment calls. They are complementary, not redundant.
- **The LLM assessment and the detector disagree**. That is normal. The LLM is subjective. The detector is deterministic. When they disagree, look at both and make a call.
- **The overlay breaks the page layout**. Rare, but some CSS can interact with the injected overlay styles. Use the [Chrome extension](https://chromewebstore.google.com/detail/impeccable/bdkgmiklpdmaojlpflclinlofgjfpabf) for the most reliable experience, or run `npx impeccable detect` from the CLI and apply findings manually.
-106
View File
@@ -1,106 +0,0 @@
---
title: Getting started
tagline: "From zero to your first polish pass in five minutes."
order: 1
description: "Install Impeccable, run /impeccable teach once to establish project context, and run /impeccable polish on something that already exists. The fastest path to seeing what Impeccable changes about AI-generated design."
---
## What you'll build
You will end this tutorial with Impeccable installed in your project, a `PRODUCT.md` plus `DESIGN.md` pair that captures your brand, audience, and visual system, and one hand-polished page that went through a polish pass. Total time: about ten minutes.
## Prerequisites
- An AI coding harness: Claude Code, Cursor, Gemini CLI, Codex CLI, or any of the other supported tools.
- A project with at least one HTML or component file you want to improve. A fresh scaffolded landing page works fine.
## How Impeccable works
Impeccable installs as a single agent skill called `impeccable`. You access all 23 sub-commands through it:
```
/impeccable <command> <target>
```
For example: `/impeccable polish the pricing page`, or `/impeccable audit the checkout`. Type `/impeccable` alone to see the full list.
If you use a command often, pin it with `/impeccable pin <command>` to create a standalone shortcut (for example, `/impeccable pin audit` gives you `/audit` directly).
## Step 1. Install
From the root of your project, run:
```
npx skills add pbakaus/impeccable
```
This auto-detects your harness and writes the skill files to the right location (e.g., `.claude/skills/`, `.cursor/skills/`). Reload your harness and type `/`. You should see `/impeccable` in the autocomplete. Type it and the skill's argument hint will show all available commands.
## Step 2. Teach Impeccable about your project
This is the most important step. Design without context produces generic output. The `/impeccable teach` command runs a short discovery interview and writes a `PRODUCT.md` file at the root of your project.
Run:
```
/impeccable teach
```
The first question is about **register**: is this a brand surface (marketing site, landing page, portfolio, where design IS the product) or a product surface (app UI, dashboard, tools, where design SERVES the product)? Register shapes every downstream default, from type lanes to motion energy. See [brand vs product](/tutorials/brand-vs-product) for how the two diverge. Teach will form a hypothesis from your codebase and ask you to confirm, rather than starting cold.
Then a handful of shorter questions:
- **Who is this product for?** Be specific. Not "users" but "solo founders evaluating a new tool on their phone between meetings".
- **What is the brand voice in three words?** Pick real words. "Warm and mechanical and opinionated" is better than "modern and clean".
- **Any visual references?** Named brands, products, or printed objects, not adjectives. "Klim Type Foundry specimen pages", not "technical and clean".
- **Anti-references?** Things the product should explicitly not look like, equally named.
Answer in your own words. The skill writes `PRODUCT.md` with the answers. Every future command run reads it automatically.
Open `PRODUCT.md` and read what it wrote. Edit anything that does not feel right. The file is yours.
## Step 2.5. Capture the visual system
At the end of `/impeccable teach`, the skill offers to run `/impeccable document` for you. Say yes. It scans your tokens (CSS custom properties, Tailwind config, CSS-in-JS themes), extracts colors and typography, asks one grouped question for the parts that need creative input (a Creative North Star, descriptive color names), and writes a `DESIGN.md` that follows the [Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/).
On a fresh project with no tokens yet, document runs in seed mode: five quick questions about color strategy, type direction, and motion energy, and writes a scaffold you can refresh once there is code.
`PRODUCT.md` carries strategy (who, what, why). `DESIGN.md` carries visuals (colors, typography, components). Every command reads both before generating.
## Step 3. Polish something
Pick a page that already exists. An about page, a settings screen, a pricing table, anything. Run:
```
/impeccable polish the pricing page
```
The skill will walk through alignment, spacing, typography, color, interaction states, transitions, and copy. It makes targeted fixes, not a rewrite. Expect a handful of small diffs that together lift the page from "done" to "done well".
A typical polish pass looks like:
```
Visual alignment: fixed 3 off-grid elements
Typography: tightened h1 kerning, fixed widow on feature list
Color: replaced one hardcoded hex with --color-accent token
Interaction: added missing hover state on FAQ items
Motion: softened modal entrance to 220ms ease-out-quart
Copy: removed stray 'Lorem' placeholder
```
Review the diff. If something does not feel right, ask the model to explain the change. If it still does not feel right, revert it. Impeccable is opinionated but not infallible.
## What to try next
- [Iterate visually with Live Mode](/tutorials/iterate-live) opens a browser picker on your dev server, generates three production-quality variants per element, and writes the accepted one back to source.
- `/impeccable critique the landing page` runs a full design review with scoring, persona tests, and automated detection. It is the best way to find what to fix next.
- `/impeccable audit the checkout` runs accessibility, performance, theming, responsive, and anti-pattern checks against the implementation. Useful before shipping.
- `/impeccable craft a pricing page for enterprise customers` runs the full shape-then-build flow on a brand new feature.
- **Pin your favorites.** If you reach for one command constantly, `/impeccable pin audit` makes `/audit` work as a standalone shortcut without reversing the consolidation.
- `/impeccable redo this hero section` works too. Any description after `/impeccable` applies the design principles to the task.
## Common issues
- **The skill says "no design context found"**. You skipped step 2. Run `/impeccable teach` first.
- **Commands do not appear in the harness**. Reload the harness after installing. If they still do not appear, check that the installer wrote files into the expected location (`.claude/skills/`, `.cursor/skills/`, etc.) and that your harness is picking up that directory.
- **The polish pass rewrote something you liked**. Say so. Revert the change, tell the model which specific edit to undo, and continue from there.
-123
View File
@@ -1,123 +0,0 @@
---
title: Iterate on UI with Live Mode
tagline: "Pick an element, generate three variants, accept one. Canvas-like iteration without leaving your code."
order: 2
description: "Use /impeccable live to visually iterate on a real element in your dev server: pick, annotate, generate three variants, accept the one you want, and have it written back to source."
---
## What you'll build
You will use `/impeccable live` on your dev server to iterate on a single piece of UI (a hero, a card, a section) and end with one of three AI-generated variants written back to source as real code. You'll see the canvas-style picking, annotation, and three-up cycling flow.
Total time: about ten minutes. Most of that is picking what to iterate on.
## Prerequisites
- Impeccable installed (see [getting started](/tutorials/getting-started) if you have not). Run `/impeccable teach` first if you haven't yet: variants lean on `PRODUCT.md` and `DESIGN.md` for brand fit.
- A running dev server with HMR (Vite, Next.js, SvelteKit, Astro, Nuxt, Bun) OR a static HTML file open in a browser.
- A page with at least one piece of UI you'd like to iterate on. A newsletter card, a hero, a pricing tier, something small enough to hold in your head.
## Step 1. Start live mode
From your harness, run:
```
/impeccable live
```
The skill starts a small local helper server on port 8400 and injects a `<script>` tag into your dev entry file that loads the picker. If your project has a strict Content Security Policy, the first run detects it and offers a one-time, dev-only patch for `script-src` and `connect-src`. Accept the patch: it is guarded by `NODE_ENV === "development"` and you can revert any time.
Open your dev server URL (not port 8400, that's the helper server, not the app). You'll see a dark pill at the bottom of the page with **Pick** highlighted.
## Step 2. Pick an element
<div class="docs-viz-step">
<div class="docs-viz-picker-row">
<div class="docs-viz-picker-target">
<span class="docs-viz-picker-pin">1</span>
Newsletter signup
<span class="docs-viz-picker-note">more playful</span>
</div>
</div>
</div>
Click the element you want to iterate on. A picker outline appears around it, and a light context bar pops up next to the selection with a command chip on the left and a freeform text field.
A few things you can do before pressing Go:
- **Click the command chip** (default is `impeccable`, the freeform action). Pick a specific action like `bolder`, `delight`, `layout`, or `typeset` to constrain the variants along one dimension.
- **Type in the freeform field.** "More playful." "Less SaaS." "Feel like a newsletter from a magazine."
- **Drop a comment pin** by clicking anywhere on the picked element. The pin's position matters: a comment near the title is about the title, not the whole element.
- **Draw a stroke** by dragging across the element. Closed loop = "this part matters." Arrow = direction. Cross = "delete this." The skill reads strokes by shape, not by pixel content.
When the brief feels clear, hit **Go**.
## Step 3. Cycle through the three variants
<div class="docs-viz-step">
<div class="docs-viz-variants">
<div class="docs-viz-variant docs-viz-variant--v1">
<span class="docs-viz-variant-badge">1 / 3</span>
<span class="docs-viz-variant-kicker">No. 04</span>
<p class="docs-viz-variant-title">Letters, <em>occasionally</em>.</p>
<span class="docs-viz-variant-btn">Send me one</span>
</div>
<div class="docs-viz-variant docs-viz-variant--v2 is-active">
<span class="docs-viz-variant-badge">2 / 3</span>
<span class="docs-viz-variant-kicker">Dispatch</span>
<p class="docs-viz-variant-title">Design notes, <br>every other<br>Thursday.</p>
<span class="docs-viz-variant-btn">Join →</span>
</div>
<div class="docs-viz-variant docs-viz-variant--v3">
<span class="docs-viz-variant-badge">3 / 3</span>
<span class="docs-viz-variant-kicker">Field Notes</span>
<p class="docs-viz-variant-title">A monthly letter, for people who still read email.</p>
<span class="docs-viz-variant-btn">Receive ✺</span>
</div>
</div>
</div>
You'll see a spinner ("Generating variants...") and within a few seconds, three variants hot-swap into the page in place. Not a preview, the actual rendered DOM on your actual dev server with your actual context.
Use the arrow keys (or the prev / next buttons on the context bar) to cycle through them. A counter at the top right shows `1 / 3`, `2 / 3`, `3 / 3`.
The three variants are designed to be **genuinely different**, not three riffs on one idea. Freeform variants anchor to three different design archetypes (broadsheet masthead, oversized-glyph poster, catalog-style spec rows, and so on). Action-specific variants vary along the dimension the action names: `colorize` gives you three hue families, `animate` gives you three motion vocabularies, `layout` gives you three structural arrangements.
If two variants feel like they rhyme, that is the skill's "squint test" failure mode. You can tell the picker "try again, all three felt too similar" and get a fresh set.
## Step 4. Accept one
<div class="docs-viz-step" style="text-align:center">
<span class="docs-viz-accept-pill">Variant 2 written to source</span>
</div>
When you find the one you like, click **Accept** on the context bar (or press Enter). Three things happen:
1. The picked element is replaced with the accepted variant on the page.
2. The variant is written back to source: the same file your picker was injected into, or the component source if live detected a generated file during step 1.
3. If the accept touched CSS, the relevant rules are consolidated into your project's real stylesheet, not left inline.
Discard all three (press Escape) and the original stays. No trace, no commented-out leftovers.
## Step 5. Stop live mode
When you are done iterating, stop the helper:
- Say **"stop live mode"** in your harness chat, or
- Click the **×** on the picker pill, or
- Close the browser tab: the helper detects the dropped connection after eight seconds and exits cleanly.
The stop also strips the `<script>` tag from your dev entry and stops the helper server on port 8400.
## What to try next
- Run `/impeccable live` on a different page after a `/impeccable polish` pass to A/B the polished version against two more directions.
- Pair with [critique with the overlay](/tutorials/critique-with-overlay): run critique first, fix priority issues, then use live to explore redirections on the element critique flagged.
- Reach for `/impeccable craft` when you want the shape-then-build flow (a new feature end-to-end, not a single element).
## Common issues
- **The picker never appears on the page.** Either the helper did not start (look for errors in the terminal) or CSP is blocking the inject. Re-run `/impeccable live` and let it re-check CSP. If you declined the patch on first run, delete the `cspChecked` line in `.impeccable/live/config.json` and re-run.
- **"element lives in a generated file"** on Go. Live detected that the picked element is in a compiled output, not a source file. It routes the accept through a fallback path so the variant still lands in true source. Follow the hint; don't force-accept into the generated file.
- **Variants don't feel brand-aligned.** Check that `PRODUCT.md` and `DESIGN.md` exist at the project root. Without them, live leans toward generic defaults. Run `/impeccable teach` and `/impeccable document` first.
- **The helper port is in use.** Another live session left its server running. `npx impeccable live stop` releases the port.
@@ -1,7 +1,7 @@
import {
FILE_DOWNLOAD_PROVIDERS,
FILE_DOWNLOAD_PROVIDER_CONFIG_DIRS
} from "../../../../../lib/download-providers.js";
} from "../../../../../cli/lib/download-providers.js";
const VALID_ID = /^[a-zA-Z0-9_-]+$/;
+1 -1
View File
@@ -1,4 +1,4 @@
import { BUNDLE_DOWNLOAD_PROVIDERS } from "../../../../lib/download-providers.js";
import { BUNDLE_DOWNLOAD_PROVIDERS } from "../../../../cli/lib/download-providers.js";
export async function onRequestGet(context) {
const { provider } = context.params;
@@ -28,7 +28,7 @@ Variants are written to the actual source file, not injected into the browser DO
Server-Sent Events (server to browser) + fetch POST (browser to server) instead of WebSocket. This eliminates the `ws` npm dependency entirely. The server is zero-dependency pure Node.js (http, crypto, fs, net, os). This matters because the scripts ship inside the skill directory and run in the user's project without any package installation.
**3. Self-contained skill scripts.**
All live mode code lives in `source/skills/impeccable/scripts/`:
All live mode code lives in `skill/scripts/`:
- `live-server.mjs` — HTTP server (SSE, poll, source file reader)
- `live-poll.mjs` — CLI client for the agent poll/reply loop
- `live-wrap.mjs` — CLI helper that finds elements in source and creates variant wrappers
@@ -91,7 +91,7 @@ For dev servers that don't support HMR (like Bun's static HTML import), the brow
┌─────────────────────────────────────────────────────────────────┐
│ AGENT │
│ │
│ Follows source/skills/impeccable/reference/live.md │
│ Follows skill/reference/live.md │
│ 1. Start server: node scripts_path/live-server.mjs & │
│ 2. Inject <script> into source HTML (comment-marked) │
│ 3. Poll loop: │
@@ -58,18 +58,18 @@ The product expectation is stronger: if the user changes live-mode state in the
### Relevant Code and Patterns
- `source/skills/impeccable/scripts/live-server.mjs` owns `/events`, `/poll`, `/source`, `/health`, token validation, in-memory `pendingEvents`, and browser SSE clients.
- `source/skills/impeccable/scripts/live-browser.js` owns picker state, variant cycling, parameter controls, `localStorage` session resume, handled-session sentinels, and accept/discard browser behavior.
- `source/skills/impeccable/scripts/live-poll.mjs` is the agent-facing poll client and auto-runs `live-accept.mjs` for accept/discard events.
- `source/skills/impeccable/scripts/live-accept.mjs` deterministically accepts/discards variant wrappers and can emit carbonize-required results.
- `source/skills/impeccable/scripts/live-wrap.mjs` creates source markers and original/variant wrapper structure.
- `skill/scripts/live-server.mjs` owns `/events`, `/poll`, `/source`, `/health`, token validation, in-memory `pendingEvents`, and browser SSE clients.
- `skill/scripts/live-browser.js` owns picker state, variant cycling, parameter controls, `localStorage` session resume, handled-session sentinels, and accept/discard browser behavior.
- `skill/scripts/live-poll.mjs` is the agent-facing poll client and auto-runs `live-accept.mjs` for accept/discard events.
- `skill/scripts/live-accept.mjs` deterministically accepts/discards variant wrappers and can emit carbonize-required results.
- `skill/scripts/live-wrap.mjs` creates source markers and original/variant wrapper structure.
- `tests/live-server.test.mjs`, `tests/live-accept.test.mjs`, `tests/live-wrap.test.mjs`, and `tests/live-e2e.test.mjs` are the relevant verification surfaces.
- `docs/adr-live-variant-mode.md` documents the current architecture and should be updated if the durable journal changes the lifecycle contract.
### Institutional Learnings
- `docs/adr-live-variant-mode.md` explicitly values source modification over DOM patching, zero-dependency scripts, SSE plus fetch, long-poll for agent compatibility, and `display: contents` wrappers.
- `source/skills/impeccable/reference/live.md` currently encodes the operational assumption that the agent continuously polls and performs carbonize cleanup before the next poll.
- `skill/reference/live.md` currently encodes the operational assumption that the agent continuously polls and performs carbonize cleanup before the next poll.
### External References
@@ -107,7 +107,7 @@ The product expectation is stronger: if the user changes live-mode state in the
## Output Structure
source/skills/impeccable/scripts/
skill/scripts/
live-session-store.mjs
live-status.mjs
live-resume.mjs
@@ -187,9 +187,9 @@ sequenceDiagram
**Dependencies:** None
**Files:**
- Create: `source/skills/impeccable/scripts/live-session-store.mjs`
- Create: `skill/scripts/live-session-store.mjs`
- Create: `tests/live-session-store.test.mjs`
- Modify: `source/skills/impeccable/scripts/live-server.mjs`
- Modify: `skill/scripts/live-server.mjs`
- Modify: `package.json`
**Approach:**
@@ -229,7 +229,7 @@ SessionSnapshot = {
```
**Patterns to follow:**
- `source/skills/impeccable/scripts/live-server.mjs` for project-root PID file handling.
- `skill/scripts/live-server.mjs` for project-root PID file handling.
- `tests/live-server.test.mjs` for temp-directory test isolation.
**Test scenarios:**
@@ -256,7 +256,7 @@ SessionSnapshot = {
**Dependencies:** U1
**Files:**
- Modify: `source/skills/impeccable/scripts/live-server.mjs`
- Modify: `skill/scripts/live-server.mjs`
- Modify: `tests/live-server.test.mjs`
**Approach:**
@@ -268,7 +268,7 @@ SessionSnapshot = {
- On server startup, rebuild pending work from the journal into the in-memory queue. `/poll` may consult the store when memory is empty, but the journal remains canonical.
**Patterns to follow:**
- Existing `validateEvent()` and `enqueueEvent()` in `source/skills/impeccable/scripts/live-server.mjs`.
- Existing `validateEvent()` and `enqueueEvent()` in `skill/scripts/live-server.mjs`.
- Existing `/events` and `/poll` tests in `tests/live-server.test.mjs`.
**Test scenarios:**
@@ -294,7 +294,7 @@ SessionSnapshot = {
**Dependencies:** U1, U2
**Files:**
- Modify: `source/skills/impeccable/scripts/live-browser.js`
- Modify: `skill/scripts/live-browser.js`
- Modify: `tests/live-e2e.test.mjs`
- Create: `tests/live-browser-recovery.test.mjs`
@@ -307,8 +307,8 @@ SessionSnapshot = {
- Capture current parameter values on every change, not only at accept time, so resume can reconstruct user tuning even before Accept.
**Patterns to follow:**
- Existing `saveSession()`, `resumeSession()`, `paramsCurrentValues`, and `handleAccept()` in `source/skills/impeccable/scripts/live-browser.js`.
- Existing scroll restoration and MutationObserver recovery patterns in `source/skills/impeccable/scripts/live-browser.js`.
- Existing `saveSession()`, `resumeSession()`, `paramsCurrentValues`, and `handleAccept()` in `skill/scripts/live-browser.js`.
- Existing scroll restoration and MutationObserver recovery patterns in `skill/scripts/live-browser.js`.
**Test scenarios:**
- Happy path: moving a parameter slider sends a checkpoint with updated param values and does not reset the tune panel.
@@ -333,11 +333,11 @@ SessionSnapshot = {
**Dependencies:** U1, U2
**Files:**
- Create: `source/skills/impeccable/scripts/live-status.mjs`
- Create: `source/skills/impeccable/scripts/live-resume.mjs`
- Create: `source/skills/impeccable/scripts/live-complete.mjs`
- Modify: `source/skills/impeccable/scripts/live-server.mjs`
- Modify: `source/skills/impeccable/reference/live.md`
- Create: `skill/scripts/live-status.mjs`
- Create: `skill/scripts/live-resume.mjs`
- Create: `skill/scripts/live-complete.mjs`
- Modify: `skill/scripts/live-server.mjs`
- Modify: `skill/reference/live.md`
- Modify: `tests/live-server.test.mjs`
- Create: `tests/live-status.test.mjs`
@@ -367,8 +367,8 @@ no_active_session
```
**Patterns to follow:**
- `source/skills/impeccable/scripts/live-poll.mjs` for CLI JSON output style.
- `source/skills/impeccable/scripts/live-accept.mjs` marker parsing helpers where reusable.
- `skill/scripts/live-poll.mjs` for CLI JSON output style.
- `skill/scripts/live-accept.mjs` marker parsing helpers where reusable.
**Test scenarios:**
- Happy path: status with a pending accept event returns `run_accept_cleanup` and includes variant id and param values.
@@ -394,9 +394,9 @@ no_active_session
**Dependencies:** U1, U2, U4
**Files:**
- Modify: `source/skills/impeccable/scripts/live-poll.mjs`
- Modify: `source/skills/impeccable/scripts/live-accept.mjs`
- Modify: `source/skills/impeccable/scripts/live-server.mjs`
- Modify: `skill/scripts/live-poll.mjs`
- Modify: `skill/scripts/live-accept.mjs`
- Modify: `skill/scripts/live-server.mjs`
- Modify: `tests/live-accept.test.mjs`
- Modify: `tests/live-server.test.mjs`
- Create: `tests/live-poll.test.mjs`
@@ -411,8 +411,8 @@ no_active_session
- Preserve the stderr warning as a human attention signal, but do not rely on warning text as the state machine.
**Patterns to follow:**
- Current `_acceptResult` attachment in `source/skills/impeccable/scripts/live-poll.mjs`.
- Current carbonize marker output in `source/skills/impeccable/scripts/live-accept.mjs`.
- Current `_acceptResult` attachment in `skill/scripts/live-poll.mjs`.
- Current carbonize marker output in `skill/scripts/live-accept.mjs`.
**Test scenarios:**
- Happy path: accept event processed by `live-poll.mjs` updates durable session to `accepted_source_pending` when carbonize is required.
@@ -437,9 +437,9 @@ no_active_session
**Dependencies:** U3, U4, U5
**Files:**
- Modify: `source/skills/impeccable/reference/live.md`
- Modify: `skill/reference/live.md`
- Modify: `docs/adr-live-variant-mode.md`
- Modify: `source/skills/impeccable/scripts/live-browser.js`
- Modify: `skill/scripts/live-browser.js`
- Modify: `README.md` if live command usage is documented there
**Approach:**
@@ -450,7 +450,7 @@ no_active_session
**Patterns to follow:**
- Existing `reference/live.md` contract sections for poll loop, accept, carbonize, and cleanup.
- Existing toast and bar state patterns in `source/skills/impeccable/scripts/live-browser.js`.
- Existing toast and bar state patterns in `skill/scripts/live-browser.js`.
**Test scenarios:**
- Test expectation: mostly documentation and UX copy. Behavioral coverage belongs to U3-U5; this unit should be verified through review plus any snapshot/E2E assertions added for visible pending states.
@@ -527,8 +527,8 @@ no_active_session
## Documentation / Operational Notes
- Update `docs/adr-live-variant-mode.md` because durability changes the architecture from memory queue plus localStorage to journaled sessions.
- Update `source/skills/impeccable/reference/live.md` so agents know to run status/resume after interruption or before assuming no pending work.
- If generated provider skill outputs are tracked, implementation should regenerate them with the existing build process after changing `source/skills/impeccable/`.
- Update `skill/reference/live.md` so agents know to run status/resume after interruption or before assuming no pending work.
- If generated provider skill outputs are tracked, implementation should regenerate them with the existing build process after changing `skill/`.
- Because the default test script enumerates test files, implementation must update `package.json` when adding new non-E2E test files.
- Consider adding `.impeccable-live/` or the chosen session-store directory to gitignore if it is not already ignored.
@@ -537,11 +537,11 @@ no_active_session
## Sources & References
- Related architecture: `docs/adr-live-variant-mode.md`
- Live instructions: `source/skills/impeccable/reference/live.md`
- Browser live implementation: `source/skills/impeccable/scripts/live-browser.js`
- Server transport: `source/skills/impeccable/scripts/live-server.mjs`
- Agent poll client: `source/skills/impeccable/scripts/live-poll.mjs`
- Accept/discard source cleanup: `source/skills/impeccable/scripts/live-accept.mjs`
- Live instructions: `skill/reference/live.md`
- Browser live implementation: `skill/scripts/live-browser.js`
- Server transport: `skill/scripts/live-server.mjs`
- Agent poll client: `skill/scripts/live-poll.mjs`
- Accept/discard source cleanup: `skill/scripts/live-accept.mjs`
- Live-mode tests: `tests/live-server.test.mjs`, `tests/live-accept.test.mjs`, `tests/live-e2e.test.mjs`
---
+5 -7
View File
@@ -26,19 +26,17 @@
"engines": {
"node": ">=18"
},
"packageManager": "bun@1.3.11",
"type": "module",
"bin": {
"impeccable": "bin/cli.js"
"impeccable": "cli/bin/cli.js"
},
"main": "./src/detect-antipatterns.mjs",
"main": "./cli/engine/detect-antipatterns.mjs",
"exports": {
".": "./src/detect-antipatterns.mjs",
"./browser": "./src/detect-antipatterns-browser.js"
".": "./cli/engine/detect-antipatterns.mjs",
"./browser": "./cli/engine/detect-antipatterns-browser.js"
},
"files": [
"bin/",
"src/",
"cli/",
"LICENSE"
],
"scripts": {
+1 -1
View File
@@ -411,7 +411,7 @@ When `_acceptResult.carbonize === true`, the accepted variant was stitched into
Do these five steps in the current thread, synchronously, before the next poll. Do not poll again until the file is clean.
1. **Locate the carbonize block** in the source file (`_acceptResult.file`). It's bracketed by `<!-- impeccable-carbonize-start SESSION_ID -->` and `<!-- impeccable-carbonize-end SESSION_ID -->` and contains a `<style data-impeccable-css="SESSION_ID">` element. If the variant declared parameters, an `<!-- impeccable-param-values SESSION_ID: {...} -->` comment sits alongside the style tag with the user's chosen values; read it first; it drives steps 3 and 4 below.
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `public/css/workflow.css` for this repo, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
2. **Move the CSS rules** into the project's real stylesheet. Which stylesheet depends on the project (e.g. `site/styles/workflow.css` for an Astro project, or the component's co-located CSS file for a Vite/Next project; pick whichever already owns styling for the surrounding element).
3. **Bake in parameter values while rewriting selectors.** For `@scope ([data-impeccable-variant="N"])` wrappers: retarget to real, semantic classes on the accepted HTML (`.why-visual--v2 .v2-label { … }`). For `:scope[data-p-<id>="VALUE"]` selectors: keep only the branch matching the chosen value from the param-values comment; drop the others (they're dead after accept). For `var(--p-<id>, DEFAULT)` in the CSS: either substitute the literal value, or if the param is still useful as a knob going forward, leave the var and update its initial declaration to the chosen value.
4. **Unwrap the accepted content.** Delete the `<div data-impeccable-variant="N" style="display: contents">` that wraps it. Drop `data-impeccable-params` and any `data-p-*` attributes from it; those are live-mode plumbing, not source.
5. **Delete the inline `<style>` block, the `<!-- impeccable-param-values -->` comment if present, and both `<!-- impeccable-carbonize-start/end -->` markers.** Also drop any `@scope` rules for variants other than the accepted one; those are dead code now.
@@ -156,11 +156,11 @@ function broadcast(msg) {
function loadBrowserScripts() {
// Detection script: look relative to the skill scripts dir, then fall back
// to the npm package location (src/detect-antipatterns-browser.js).
// to the npm package location (cli/engine/detect-antipatterns-browser.js).
// This one IS cached — detect.js rarely changes during a session.
const detectPaths = [
path.join(__dirname, '..', '..', '..', '..', 'src', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'src', 'detect-antipatterns-browser.js'),
path.join(__dirname, '..', '..', '..', '..', 'cli', 'engine', 'detect-antipatterns-browser.js'),
path.join(process.cwd(), 'node_modules', 'impeccable', 'cli', 'engine', 'detect-antipatterns-browser.js'),
];
let detectScript = '';
for (const p of detectPaths) {
+3 -3
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node
/**
* Generates src/detect-antipatterns-browser.js
* Generates cli/engine/detect-antipatterns-browser.js
* by stripping Node-specific sections from the universal source and wrapping in an IIFE.
*
* Run: node scripts/build-browser-detector.js
@@ -14,8 +14,8 @@ import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const SOURCE = path.join(ROOT, 'src/detect-antipatterns.mjs');
const OUTPUT = path.join(ROOT, 'src/detect-antipatterns-browser.js');
const SOURCE = path.join(ROOT, 'cli/engine/detect-antipatterns.mjs');
const OUTPUT = path.join(ROOT, 'cli/engine/detect-antipatterns-browser.js');
let code = fs.readFileSync(SOURCE, 'utf-8');
+1 -1
View File
@@ -18,7 +18,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const EXT_DIR = path.join(ROOT, 'extension');
const SOURCE = path.join(ROOT, 'src/detect-antipatterns.mjs');
const SOURCE = path.join(ROOT, 'cli/engine/detect-antipatterns.mjs');
const DETECTOR_OUTPUT = path.join(EXT_DIR, 'detector/detect.js');
const AP_OUTPUT = path.join(EXT_DIR, 'detector/antipatterns.json');
+18 -110
View File
@@ -22,10 +22,9 @@ import { readSourceFiles, readPatterns, stashPerProjectArtifacts, restorePerProj
import { createTransformer, PROVIDERS } from './lib/transformers/index.js';
import { createAllZips } from './lib/zip.js';
// Sub-page generation is now handled by Astro content collections.
// import { generateSubPages } from './build-sub-pages.js';
/**
* Generate authoritative counts from source data and write to public/js/generated/counts.js.
* Generate authoritative counts from source data and write to site/public/js/generated/counts.js.
* Also validates that key HTML files reference the correct numbers.
*/
function generateCounts(rootDir, skills, buildDir) {
@@ -50,7 +49,7 @@ function generateCounts(rootDir, skills, buildDir) {
}
// Count detection rules from impeccable package
const detectPkgPath = path.join(rootDir, 'src/detect-antipatterns.mjs');
const detectPkgPath = path.join(rootDir, 'cli/engine/detect-antipatterns.mjs');
const detectorSrc = fs.readFileSync(detectPkgPath, 'utf-8');
const ruleIds = new Set();
for (const match of detectorSrc.matchAll(/^\s+id: '([^']+)'/gm)) {
@@ -59,7 +58,7 @@ function generateCounts(rootDir, skills, buildDir) {
const detectionCount = ruleIds.size;
// Write generated counts module
const genDir = path.join(rootDir, 'public/js/generated');
const genDir = path.join(rootDir, 'site/public/js/generated');
fs.mkdirSync(genDir, { recursive: true });
fs.writeFileSync(path.join(genDir, 'counts.js'),
`// GENERATED by build.js — do not edit\n` +
@@ -69,7 +68,7 @@ function generateCounts(rootDir, skills, buildDir) {
// Validate counts in key files
const filesToCheck = [
'public/index.html',
'site/pages/index.astro',
'README.md',
'NOTICE.md',
'AGENTS.md',
@@ -137,7 +136,7 @@ function validateSkillFrontmatter(skills) {
* The denylist is the editorial brief in STYLE.md, enforced. Each rule has a
* rationale that prints with the failure so the next author understands why.
*
* Scope: every surface a reader sees. Not source/skills/impeccable/, where
* Scope: every surface a reader sees. Not skill/, where
* LLM-facing reference instructions can use technical phrasings the marketing
* copy can't.
*
@@ -145,7 +144,6 @@ function validateSkillFrontmatter(skills) {
*/
function validateProse(rootDir) {
const targets = [
'content/site',
'site/components',
'site/content',
'site/layouts',
@@ -247,7 +245,7 @@ function validateProse(rootDir) {
* Returns the number of occurrences found. Build fails if > 0.
*/
function validateSkillProse(rootDir) {
const target = 'source/skills/impeccable';
const target = 'skill';
const extensions = new Set(['.md']);
const emDashPatterns = [/—/g, /&mdash;/gi, /&#8212;/gi, /&#x2014;/gi];
// Tighter than validateProse: only the rules that have no technical reading.
@@ -314,9 +312,9 @@ function validateSkillProse(rootDir) {
if (fs.existsSync(full)) scan(full, target);
if (errors === 0) {
console.log(`✓ Skill prose validator: source/skills/impeccable/ is clean`);
console.log(`✓ Skill prose validator: skill/ is clean`);
} else {
console.error(`\n${errors} prose issue(s) in source/skills/impeccable/. See STYLE.md.`);
console.error(`\n${errors} prose issue(s) in skill/. See STYLE.md.`);
}
return errors;
}
@@ -360,96 +358,6 @@ const DIST_DIR = path.join(ROOT_DIR, 'dist');
// buildStaticSite (Bun HTML bundler) removed — now handled by Astro.
// Placeholder so the line-number-based edits below don't shift.
async function _REMOVED() {
const entrypoints = [
path.join(ROOT_DIR, 'public', 'index.html'),
path.join(ROOT_DIR, 'public', 'privacy.html'),
...extraEntrypoints,
];
const outdir = path.join(ROOT_DIR, 'build');
console.log(`📦 Building static site with Bun (${entrypoints.length} HTML entries)...`);
try {
const result = await Bun.build({
entrypoints: entrypoints,
outdir: outdir,
minify: true,
sourcemap: 'linked',
// Older Bun versions (e.g. the one Cloudflare Pages ships) don't dedupe
// shared CSS/JS chunks across HTML entrypoints — every entry tries to
// emit its own copy, and three different sub-pages all named index.html
// (under docs/, tutorials/, slop/) collide on the same
// chunk filename. Including [dir] in the chunk template scopes each
// chunk to its entry's directory so the names stay unique even when
// dedupe is off. Local Bun still emits a single shared chunk; CF Bun
// emits one per entry but each lands in its own directory.
naming: {
entry: '[dir]/[name].[ext]',
chunk: '[dir]/[name]-[hash].[ext]',
asset: '[dir]/[name]-[hash].[ext]',
},
});
if (!result.success) {
console.error('Build failed:');
for (const log of result.logs) {
console.error(log.message || log);
if (log.position) {
console.error(` at ${log.position.file}:${log.position.line}:${log.position.column}`);
}
}
process.exit(1);
}
// Calculate total size
const totalSize = result.outputs.reduce((sum, o) => sum + o.size, 0);
const htmlFiles = result.outputs.filter(o => o.path.endsWith('.html'));
const jsFiles = result.outputs.filter(o => o.path.endsWith('.js'));
const cssFiles = result.outputs.filter(o => o.path.endsWith('.css'));
// When entrypoints span multiple depths under public/ (e.g. public/index.html
// + public/docs/polish.html), Bun's HTML loader preserves the full public/
// prefix in the output tree. Flatten build/public/* up to build/*.
const nestedPublic = path.join(outdir, 'public');
if (fs.existsSync(nestedPublic)) {
for (const entry of fs.readdirSync(nestedPublic, { withFileTypes: true })) {
const from = path.join(nestedPublic, entry.name);
const to = path.join(outdir, entry.name);
if (fs.existsSync(to)) fs.rmSync(to, { recursive: true, force: true });
fs.renameSync(from, to);
}
fs.rmdirSync(nestedPublic);
}
console.log(`✓ Static site built to ./build/`);
console.log(` HTML: ${htmlFiles.length} file(s)`);
console.log(` JS: ${jsFiles.length} file(s) (${(jsFiles.reduce((s, f) => s + f.size, 0) / 1024).toFixed(1)} KB)`);
console.log(` CSS: ${cssFiles.length} file(s) (${(cssFiles.reduce((s, f) => s + f.size, 0) / 1024).toFixed(1)} KB)`);
console.log(` Total: ${(totalSize / 1024).toFixed(1)} KB\n`);
return result;
} catch (error) {
// Bun's build aggregator errors expose details on `error.errors` (an
// array of resolution / parse failures), not `error.stack`. Print
// both so CI logs surface the real cause instead of "undefined".
console.error('Failed to build static site:', error.message);
if (error.errors?.length) {
for (const e of error.errors) {
console.error(' -', e.message || e);
}
}
if (error.logs?.length) {
for (const log of error.logs) {
console.error(log.message || log);
}
}
if (error.stack) console.error(error.stack);
process.exit(1);
}
}
/**
* Assemble universal directory from all provider outputs
*/
@@ -521,11 +429,11 @@ function generateApiData(buildDir, skills, patterns) {
// /impeccable. Load them from command-metadata.json and include the root
// impeccable skill itself so UI surfaces like the cheatsheet can list them.
// Each entry also picks up a short `tagline` from its editorial file
// (content/site/skills/<id>.md) when one exists. Taglines are used by UI
// (site/content/skills/<id>.md) when one exists. Taglines are used by UI
// surfaces that need a human-friendly one-liner, while `description` stays
// optimized for auto-trigger keyword matching in the AI harness.
const readTagline = (id) => {
const editorialPath = path.join(ROOT_DIR, 'content/site/skills', `${id}.md`);
const editorialPath = path.join(ROOT_DIR, 'site/content/skills', `${id}.md`);
if (!fs.existsSync(editorialPath)) return null;
const raw = fs.readFileSync(editorialPath, 'utf-8');
const match = raw.match(/^---\n([\s\S]*?)\n---/);
@@ -534,13 +442,13 @@ function generateApiData(buildDir, skills, patterns) {
return taglineMatch ? taglineMatch[1] : null;
};
const metadataPath = path.join(ROOT_DIR, 'source/skills/impeccable/scripts/command-metadata.json');
const metadataPath = path.join(ROOT_DIR, 'skill/scripts/command-metadata.json');
if (!fs.existsSync(metadataPath)) {
throw new Error(`command-metadata.json is missing at ${metadataPath}. This file is required to generate the commands API.`);
}
const impeccable = skills.find(s => s.name === 'impeccable');
if (!impeccable) {
throw new Error('impeccable skill not found in source/skills/. The build system expects a single impeccable skill.');
throw new Error('impeccable skill not found at skill/SKILL.md. The build system expects exactly one skill at that path.');
}
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
@@ -688,11 +596,11 @@ async function build() {
// handled by Astro (bun run build:site). This script focuses on skills,
// API data, and Cloudflare config.
// Copy browser detector to public/js/ so the antipattern examples can
// reference it (Astro serves public/ as-is).
const detectorSrc = path.join(ROOT_DIR, 'src', 'detect-antipatterns-browser.js');
// Copy browser detector to site/public/js/ so the antipattern examples can
// reference it (Astro serves site/public/ as-is).
const detectorSrc = path.join(ROOT_DIR, 'cli', 'engine', 'detect-antipatterns-browser.js');
if (fs.existsSync(detectorSrc)) {
const jsDir = path.join(ROOT_DIR, 'public', 'js');
const jsDir = path.join(ROOT_DIR, 'site', 'public', 'js');
fs.mkdirSync(jsDir, { recursive: true });
fs.copyFileSync(detectorSrc, path.join(jsDir, 'detect-antipatterns-browser.js'));
}
@@ -727,10 +635,10 @@ async function build() {
await createAllZips(DIST_DIR);
// Generate static API data and Cloudflare Pages config
// Write API data and CF config to public/ so Astro copies them to build/.
// Write API data and CF config to site/public/ so Astro copies them to build/.
// Astro wipes build/ before writing, so anything written directly to build/
// during build:skills would be destroyed when build:site runs.
const publicDir = path.join(ROOT_DIR, 'public');
const publicDir = path.join(ROOT_DIR, 'site', 'public');
generateApiData(publicDir, skills, patterns);
generateCFConfig(publicDir);
+9 -23
View File
@@ -19,7 +19,7 @@ import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ROOT_DIR = path.resolve(__dirname, '..');
const OUTPUT_PATH = path.join(ROOT_DIR, 'public', 'og-image.jpg');
const OUTPUT_PATH = path.join(ROOT_DIR, 'site', 'public', 'og-image.jpg');
const EXTENSION_IMAGE_PATH = path.join(
ROOT_DIR,
'public',
@@ -27,29 +27,15 @@ const EXTENSION_IMAGE_PATH = path.join(
'extension-detection.png',
);
// Count user-invocable, non-deprecated skills from source/skills/
// (In v2.0, commands and skills were unified — every command is a skill.)
// Count sub-commands from skill/scripts/command-metadata.json (the post-v3.0
// single source of truth). Commands and skills were unified in v2.0; v3.0
// then collapsed to a single user-invocable skill (`impeccable`) with
// sub-commands listed in command-metadata.json.
function getCommandCount() {
const skillsDir = path.join(ROOT_DIR, 'source', 'skills');
if (!fs.existsSync(skillsDir)) return 0;
let count = 0;
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const skillFile = path.join(skillsDir, entry.name, 'SKILL.md');
if (!fs.existsSync(skillFile)) continue;
const content = fs.readFileSync(skillFile, 'utf8');
const fm = content.match(/^---\n([\s\S]*?)\n---/);
if (!fm) continue;
const frontmatter = fm[1];
const isUserInvocable = /^user-invocable:\s*true\s*$/m.test(frontmatter);
const isDeprecated = /^description:\s*["']?DEPRECATED/mi.test(frontmatter);
if (isUserInvocable && !isDeprecated) count++;
}
return count;
const metadataPath = path.join(ROOT_DIR, 'skill', 'scripts', 'command-metadata.json');
if (!fs.existsSync(metadataPath)) return 0;
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
return Object.keys(metadata).length;
}
// Load extension screenshot as base64 data URL so setContent is self-contained
-156
View File
@@ -1,156 +0,0 @@
/**
* Markdown HTML rendering for sub-pages.
*
* Wraps `marked` with a custom link renderer that resolves cross-references
* between skill bodies and their references, and emits stable heading slugs
* so anti-pattern skill section anchors work.
*
* Skeleton in commit 1. Link resolution and heading slugger are wired up in
* commit 3 (skills generator) when the data model lands.
*/
import { marked } from 'marked';
/**
* Slugify a heading text into a stable anchor id.
* Matches the convention: lowercase, strip non-alphanum, spaces dashes.
*
* @param {string} text
* @returns {string}
*/
export function slugify(text) {
return String(text)
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_]+/g, '-')
.replace(/^-+|-+$/g, '');
}
/**
* Build a marked renderer configured for impeccable's skill/tutorial bodies.
*
* @param {object} opts
* @param {Set<string>} [opts.knownSkillIds] - slugs of skills the site knows about; unknown /name mentions render as plain text
* @param {string} [opts.currentSkillId] - when rendering a skill body, resolve `reference/foo.md` to `#reference-foo` on the current page
* @returns {import('marked').Renderer}
*/
export function createRenderer({ knownSkillIds = new Set(), currentSkillId = null } = {}) {
const renderer = new marked.Renderer();
// Heading slugger — stable ids so we can anchor-link from elsewhere.
// Supports {#custom-id} suffix (kramdown/pandoc style) for explicit anchors.
renderer.heading = ({ tokens, depth }) => {
const raw = tokens.map((t) => t.raw || '').join('');
const customIdMatch = raw.match(/\s*\{#([a-z0-9_-]+)\}\s*$/i);
let id, text;
if (customIdMatch) {
id = customIdMatch[1];
// Strip the {#id} suffix from the rendered text
const cleanRaw = raw.slice(0, customIdMatch.index);
text = renderer.parser.parseInline(marked.lexer(cleanRaw, { gfm: true })[0]?.tokens || tokens);
} else {
id = slugify(raw);
text = renderer.parser.parseInline(tokens);
}
return `<h${depth} id="${id}">${text}</h${depth}>\n`;
};
// Link resolver.
renderer.link = ({ href, title, tokens }) => {
const text = renderer.parser.parseInline(tokens);
const resolved = resolveHref(href, { knownSkillIds, currentSkillId });
const titleAttr = title ? ` title="${escapeAttr(title)}"` : '';
const relAttr = resolved.external ? ' target="_blank" rel="noopener"' : '';
return `<a href="${escapeAttr(resolved.href)}"${titleAttr}${relAttr}>${text}</a>`;
};
// Fenced code blocks — minimal glass-terminal styling, no syntax highlighter in v1.
// Wrapped in a container with a copy button; click handling lives in the
// page-level inline script added by render-page.js.
renderer.code = ({ text, lang }) => {
const langClass = lang ? ` code-block--${escapeAttr(lang)}` : '';
const copyValue = escapeAttr(text);
return `<div class="code-block-wrap"><pre class="code-block${langClass}"><code>${escapeHtml(text)}</code></pre><button class="code-block-copy" type="button" data-copy="${copyValue}" aria-label="Copy to clipboard"></button></div>\n`;
};
return renderer;
}
/**
* Resolve a markdown link href against the site's URL scheme.
*
* - `http(s)://…` unchanged, external
* - `reference/foo.md` `#reference-foo` on current skill page
* - `/skill-id` (known) `/docs/skill-id`
* - `#anchor` unchanged (in-page anchor)
* - anything else unchanged (will be caught by build warnings later)
*
* @param {string} href
* @param {{ knownSkillIds: Set<string>, currentSkillId: string|null }} ctx
* @returns {{ href: string, external: boolean }}
*/
function resolveHref(href, { knownSkillIds, currentSkillId }) {
if (!href) return { href: '', external: false };
// External links
if (/^https?:\/\//i.test(href) || /^mailto:/i.test(href)) {
return { href, external: true };
}
// In-page anchor
if (href.startsWith('#')) {
return { href, external: false };
}
// reference/foo.md → #reference-foo on the current skill page
const refMatch = href.match(/^reference\/([a-z0-9-]+)\.md$/i);
if (refMatch && currentSkillId) {
return { href: `#reference-${refMatch[1].toLowerCase()}`, external: false };
}
// /skill-id mentioned in prose (e.g. "run /polish")
const slashMatch = href.match(/^\/([a-z0-9-]+)$/i);
if (slashMatch && knownSkillIds.has(slashMatch[1])) {
return { href: `/docs/${slashMatch[1]}`, external: false };
}
// [text](other-skill) → /docs/other-skill
if (/^[a-z0-9-]+$/i.test(href) && knownSkillIds.has(href)) {
return { href: `/docs/${href}`, external: false };
}
// Unknown — pass through. Generator can warn separately.
return { href, external: false };
}
/**
* Render a markdown string to HTML.
*
* @param {string} markdown
* @param {object} [opts]
* @param {Set<string>} [opts.knownSkillIds]
* @param {string} [opts.currentSkillId]
* @returns {string} HTML
*/
export function renderMarkdown(markdown, opts = {}) {
const renderer = createRenderer(opts);
return marked.parse(markdown, {
renderer,
gfm: true,
breaks: false,
});
}
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function escapeAttr(str) {
return String(str).replace(/"/g, '&quot;');
}
+13 -13
View File
@@ -3,11 +3,11 @@
* generators.
*
* Single source of truth:
* - source/skills/{id}/SKILL.md skill frontmatter + body
* - source/skills/{id}/reference/*.md skill reference files
* - src/detect-antipatterns.mjs ANTIPATTERNS array (parsed)
* - content/site/skills/{id}.md optional editorial wrapper
* - content/site/tutorials/{slug}.md full tutorial content
* - skill/SKILL.md skill frontmatter + body
* - skill/reference/*.md skill reference files
* - cli/engine/detect-antipatterns.mjs ANTIPATTERNS array (parsed)
* - site/content/skills/{id}.md optional editorial wrapper
* - site/content/tutorials/{slug}.md full tutorial content
*/
import fs from 'node:fs';
@@ -19,13 +19,13 @@ import {
VISUAL_EXAMPLES,
LLM_ONLY_RULES,
GALLERY_ITEMS,
} from '../../content/site/anti-patterns-catalog.js';
} from '../../site/data/anti-patterns-catalog.js';
export {
LAYER_LABELS,
LAYER_DESCRIPTIONS,
GALLERY_ITEMS,
} from '../../content/site/anti-patterns-catalog.js';
} from '../../site/data/anti-patterns-catalog.js';
/**
* Skills that should be excluded from the index and not get a detail page.
@@ -137,12 +137,12 @@ export const COMMAND_RELATIONSHIPS = {
};
/**
* Parse the ANTIPATTERNS array out of src/detect-antipatterns.mjs.
* Parse the ANTIPATTERNS array out of cli/engine/detect-antipatterns.mjs.
* Mirrors the trick in scripts/build.js validateAntipatternRules() so we
* don't have to run the browser-only module.
*/
export function readAntipatternRules(rootDir) {
const detectPath = path.join(rootDir, 'src/detect-antipatterns.mjs');
const detectPath = path.join(rootDir, 'cli/engine/detect-antipatterns.mjs');
const src = fs.readFileSync(detectPath, 'utf-8');
const match = src.match(/const ANTIPATTERNS = \[([\s\S]*?)\n\];/);
if (!match) {
@@ -170,7 +170,7 @@ export function readEditorialWrapper(contentDir, kind, slug) {
* should treat a missing entry as "no demo".
*/
export async function loadCommandDemos(rootDir) {
const demosDir = path.join(rootDir, 'public/js/demos/commands');
const demosDir = path.join(rootDir, 'site/public/js/demos/commands');
if (!fs.existsSync(demosDir)) return {};
const demos = {};
@@ -210,7 +210,7 @@ export async function loadCommandDemos(rootDir) {
*/
export async function buildSubPageData(rootDir) {
const { skills: rawSkills } = readSourceFiles(rootDir);
const contentDir = path.join(rootDir, 'content/site');
const contentDir = path.join(rootDir, 'site/content');
const commandDemos = await loadCommandDemos(rootDir);
// After the v3.0 consolidation there's only one source skill (impeccable).
@@ -218,7 +218,7 @@ export async function buildSubPageData(rootDir) {
// We synthesize a virtual skill entry for each sub-command so the sub-page
// generators can keep rendering per-command pages, index cards, etc.
const impeccableSkill = rawSkills.find((s) => s.name === 'impeccable');
const metadataPath = path.join(rootDir, 'source/skills/impeccable/scripts/command-metadata.json');
const metadataPath = path.join(rootDir, 'skill/scripts/command-metadata.json');
let commandMetadata = {};
if (fs.existsSync(metadataPath)) {
commandMetadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
@@ -310,7 +310,7 @@ export async function buildSubPageData(rootDir) {
}));
const rules = [...detectedRules, ...llmRules];
// Tutorials: each required file in content/site/tutorials/.
// Tutorials: each required file in site/content/tutorials/.
const tutorialsDir = path.join(contentDir, 'tutorials');
const tutorials = [];
if (fs.existsSync(tutorialsDir)) {
+59 -72
View File
@@ -153,85 +153,72 @@ export function readFilesRecursive(dir, fileList = []) {
}
/**
* Read and parse all source files (unified skills architecture)
* All source lives in source/skills/{name}/SKILL.md
* Returns { skills } where each skill has userInvocable flag
* Read and parse the impeccable skill source.
* After v3.0 the repo holds exactly one user-invocable skill, flat at skill/.
* Returns { skills: [oneEntry] } so downstream array-shaped consumers stay happy.
*/
export function readSourceFiles(rootDir) {
const skillsDir = path.join(rootDir, 'source/skills');
const skillDir = path.join(rootDir, 'skill');
const skills = [];
if (fs.existsSync(skillsDir)) {
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
const skillMdPath = path.join(skillDir, 'SKILL.md');
if (!fs.existsSync(skillMdPath)) {
return { skills };
}
for (const entry of entries) {
const entryPath = path.join(skillsDir, entry.name);
const content = fs.readFileSync(skillMdPath, 'utf-8');
const { frontmatter, body } = parseFrontmatter(content);
if (entry.isDirectory()) {
// Directory-based skill with potential references
const skillMdPath = path.join(entryPath, 'SKILL.md');
if (fs.existsSync(skillMdPath)) {
const content = fs.readFileSync(skillMdPath, 'utf-8');
const { frontmatter, body } = parseFrontmatter(content);
// Read reference files if they exist
const references = [];
const referenceDir = path.join(entryPath, 'reference');
if (fs.existsSync(referenceDir)) {
const refFiles = fs.readdirSync(referenceDir).filter(f => f.endsWith('.md'));
for (const refFile of refFiles) {
const refPath = path.join(referenceDir, refFile);
const refContent = fs.readFileSync(refPath, 'utf-8');
references.push({
name: path.basename(refFile, '.md'),
content: refContent,
filePath: refPath
});
}
}
// Read script files if they exist. PER_PROJECT_SCRIPT_ARTIFACTS
// (defined at module top) are excluded from the distributable skill
// so the build never bundles one project's state into another's.
const scripts = [];
const scriptsDir = path.join(entryPath, 'scripts');
if (fs.existsSync(scriptsDir)) {
const scriptFiles = fs.readdirSync(scriptsDir).filter(f => {
if (PER_PROJECT_SCRIPT_ARTIFACTS.has(f)) return false;
return fs.statSync(path.join(scriptsDir, f)).isFile();
});
for (const scriptFile of scriptFiles) {
const scriptPath = path.join(scriptsDir, scriptFile);
const scriptContent = fs.readFileSync(scriptPath, 'utf-8');
scripts.push({
name: scriptFile,
content: scriptContent,
filePath: scriptPath
});
}
}
skills.push({
name: frontmatter.name || entry.name,
description: frontmatter.description || '',
license: frontmatter.license || '',
compatibility: frontmatter.compatibility || '',
metadata: frontmatter.metadata || null,
allowedTools: frontmatter['allowed-tools'] || '',
userInvocable: frontmatter['user-invocable'] === true || frontmatter['user-invocable'] === 'true',
argumentHint: frontmatter['argument-hint'] || '',
context: frontmatter.context || null,
body,
filePath: skillMdPath,
references,
scripts
});
}
}
const references = [];
const referenceDir = path.join(skillDir, 'reference');
if (fs.existsSync(referenceDir)) {
const refFiles = fs.readdirSync(referenceDir).filter(f => f.endsWith('.md'));
for (const refFile of refFiles) {
const refPath = path.join(referenceDir, refFile);
references.push({
name: path.basename(refFile, '.md'),
content: fs.readFileSync(refPath, 'utf-8'),
filePath: refPath
});
}
}
// PER_PROJECT_SCRIPT_ARTIFACTS (defined at module top) are excluded from
// the distributable skill so the build never bundles one project's state
// into another's.
const scripts = [];
const scriptsDir = path.join(skillDir, 'scripts');
if (fs.existsSync(scriptsDir)) {
const scriptFiles = fs.readdirSync(scriptsDir).filter(f => {
if (PER_PROJECT_SCRIPT_ARTIFACTS.has(f)) return false;
return fs.statSync(path.join(scriptsDir, f)).isFile();
});
for (const scriptFile of scriptFiles) {
const scriptPath = path.join(scriptsDir, scriptFile);
scripts.push({
name: scriptFile,
content: fs.readFileSync(scriptPath, 'utf-8'),
filePath: scriptPath
});
}
}
skills.push({
name: frontmatter.name || 'impeccable',
description: frontmatter.description || '',
license: frontmatter.license || '',
compatibility: frontmatter.compatibility || '',
metadata: frontmatter.metadata || null,
allowedTools: frontmatter['allowed-tools'] || '',
userInvocable: frontmatter['user-invocable'] === true || frontmatter['user-invocable'] === 'true',
argumentHint: frontmatter['argument-hint'] || '',
context: frontmatter.context || null,
body,
filePath: skillMdPath,
references,
scripts
});
return { skills };
}
@@ -269,7 +256,7 @@ export function writeFile(filePath, content) {
* - Prose form: `DO …` / `DO NOT …`
*
* Defaults to the main impeccable SKILL.md but accepts any relative path so
* rules in `src/detect-antipatterns.mjs` can anchor to register-specific
* rules in `cli/engine/detect-antipatterns.mjs` can anchor to register-specific
* reference files (e.g. `reference/editorial.md`) via an optional `skillFile`
* field. Callers that don't pass `relativePath` get the legacy behavior.
*
@@ -369,7 +356,7 @@ export function readPatterns(_rootDir, _relativePath) {
// Previous SKILL.md parser retained below but disabled; kept as a
// reference for how prefix-style extraction used to work.
function _legacyReadPatterns(rootDir, relativePath = 'source/skills/impeccable/SKILL.md') {
function _legacyReadPatterns(rootDir, relativePath = 'skill/SKILL.md') {
const skillPath = path.join(rootDir, relativePath);
if (!fs.existsSync(skillPath)) {
+2 -2
View File
@@ -15,8 +15,8 @@ import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ROOT_DIR = path.resolve(__dirname, '..');
const EXAMPLES_DIR = path.join(ROOT_DIR, 'public', 'antipattern-examples');
const OUTPUT_DIR = path.join(ROOT_DIR, 'public', 'antipattern-images');
const EXAMPLES_DIR = path.join(ROOT_DIR, 'site', 'public', 'antipattern-examples');
const OUTPUT_DIR = path.join(ROOT_DIR, 'site', 'public', 'antipattern-images');
async function screenshotAntipatterns() {
// Ensure output directory exists
@@ -1,7 +1,7 @@
/**
* Manual metadata for the /anti-patterns page.
*
* The detection rules themselves live in src/detect-antipatterns.mjs and
* The detection rules themselves live in cli/engine/detect-antipatterns.mjs and
* are parsed at build time. This file adds three pieces of content that
* can't be automated:
*
@@ -15,7 +15,7 @@
* cream/paper/ink palette when possible, and sit naturally at
* ~100% width by ~120px height.
*
* 3. LLM_ONLY_RULES: DON'T lines from source/skills/impeccable/SKILL.md
* 3. LLM_ONLY_RULES: DON'T lines from skill/SKILL.md
* that do not map to any detection rule. These can only be caught by
* the /critique skill's LLM pass. They appear on the /anti-patterns
* page alongside detected rules with an 'llm' layer badge.
+1 -1
View File
@@ -8,7 +8,7 @@ import { SKILL_CATEGORIES } from '../../data/sub-pages-data';
export async function getStaticPaths() {
const entries = await getCollection('skills');
const metadataPath = path.join(process.cwd(), 'source/skills/impeccable/scripts/command-metadata.json');
const metadataPath = path.join(process.cwd(), 'skill/scripts/command-metadata.json');
const commandMetadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
const allCommands = entries.map(e => ({

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 181 KiB

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Before

Width:  |  Height:  |  Size: 968 KiB

After

Width:  |  Height:  |  Size: 968 KiB

Before

Width:  |  Height:  |  Size: 696 KiB

After

Width:  |  Height:  |  Size: 696 KiB

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Some files were not shown because too many files have changed in this diff Show More