Compare commits

...
Author SHA1 Message Date
Paul Bakaus a79063a385 Merge main into plugin path fix
Regenerate the intentionally shipped plugin subtree and keep unrelated harness output out of the feature branch.\n\nAI-assisted: conflicts resolved and validated by Codex under maintainer direction.
2026-08-31 17:08:09 -07:00
Abdul WahabandCursor 3611594092 Define the agent embed behavior when the plugin-root variable is unexpanded
The current plugins reference substitutes ${CLAUDE_PLUGIN_ROOT} inline
anywhere in plugin skill and agent content, superseding the hook-only
note this module carried from earlier research (review finding cited it).
The module header now states the actual contract, and because Grok Build
reads the same subtree with its own substitution behavior, the agent's
embed instruction gains a defined fallback: when the script path is
unreachable, the prompt survives as an <asset>.prompt.txt sidecar and the
manifest tells the parent, whose thread can resolve the script and embed
properly. A new build guard fails when an agent file carries an
unresolvable path form or an embed instruction whose fallback anchor
drifted.

Drafted with AI assistance, reviewed by a maintainer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 05:18:06 +05:00
Abdul WahabandCursor d28a9dc31c Give plugin agent files the plugin-root variable, not the skill token
A spawned agent never loads SKILL.md, so the <skill-base-dir> token
Setup defines is undefined in the one context that must act on it
(review finding). Claude Code substitutes ${CLAUDE_PLUGIN_ROOT} inline
anywhere in plugin agent content, so the agents directory now gets its
own rewrite emitting the quoted variable form; the skill and reference
files keep the token, which the main thread's base-directory report
resolves.

Drafted with AI assistance, reviewed by a maintainer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 01:08:16 +05:00
Abdul WahabandCursor abf7773c3d Fix: stop the plugin subtree from running the project's skill copy (#523)
The ./plugin subtree copied dist/claude-code output verbatim, so its 44
markdown instructions said `node .claude/skills/impeccable/scripts/...`,
a path inside the user's project. A plugin-only install got
MODULE_NOT_FOUND; a dual install silently ran the project's older skill
copy with zero indication anything was wrong.

The build now rewrites the plugin subtree's markdown after the copy:

- Every script instruction takes the quoted `node "<skill-base-dir>/
  scripts/..."` form (quoted because the loaded plugin directory can
  contain spaces), resolving against the base directory the runtime
  reports, since no literal path works for plugins (CLAUDE_PLUGIN_ROOT
  is hook-only).
- Setup step 1 loses its project-path fallback clause: a plugin install
  has no working project fallback, that path being the bug itself.
- The `Bash(node .claude/skills/impeccable/scripts/*)` allowed-tools
  entry is dropped rather than widened: a wildcard such as
  `node */skills/impeccable/scripts/*` would auto-approve any
  same-shaped path anywhere on disk (per review), and frontmatter has
  no variable bound to the plugin root. Script runs go through the
  normal Bash confirmation.
- A build-time guard fails build:release when SKILL.src.md's wording
  drifts past the rewrite, when a node pre-approval reappears, or when
  any project-relative scripts path survives in the plugin SKILL.md.

Drafted with AI assistance, reviewed by a maintainer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 00:52:12 +05:00
18 changed files with 551 additions and 50 deletions
+2 -2
View File
@@ -30,8 +30,8 @@ Every region with `medium: raster` in the spec ships as a plate at its `plate` p
Per region, in the spec's order: Per region, in the spec's order:
1. `node .claude/skills/impeccable/scripts/comp-spec.mjs --crop <id>` writes the reference crop under `.impeccable/build/crops/`. 1. `node "${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/comp-spec.mjs" --crop <id>` writes the reference crop under `.impeccable/build/crops/`.
2. Produce the plate. With the API fallback: `node .claude/skills/impeccable/scripts/generate-image.mjs --plate <id> --quality high` does the whole step (crop as reference, the spec's plate prompt, output size chosen from the region's aspect, the file written to its plate path, prompt embedded, and the plate scored against the crop). With a harness-native image tool: use the crop as the input image and `node .claude/skills/impeccable/scripts/comp-spec.mjs --plate-prompt <id>` as the prompt, write the result to the plate path, then run `node .claude/skills/impeccable/scripts/embed-prompt.mjs <plate> --prompt "<the exact prompt>"`. 2. Produce the plate. With the API fallback: `node "${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/generate-image.mjs" --plate <id> --quality high` does the whole step (crop as reference, the spec's plate prompt, output size chosen from the region's aspect, the file written to its plate path, prompt embedded, and the plate scored against the crop). With a harness-native image tool: use the crop as the input image and `node "${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/comp-spec.mjs" --plate-prompt <id>` as the prompt, write the result to the plate path, then run `node "${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/embed-prompt.mjs" <plate> --prompt "<the exact prompt>"`. When that script path is unreachable in your environment, write the same prompt to `<asset>.prompt.txt` beside the asset and note it in your manifest so the parent can embed it.
3. Read the score line. `PLATE-SCORE` under 50%, or a `PLATE-WARN`, means the plate does not read as the region: open the plate beside the crop, name what drifted (subject, framing, palette, style), tighten the prompt with that, and regenerate once. Two misses on one region: keep the better plate, mark it `needs_parent_review`, and say why in one line. 3. Read the score line. `PLATE-SCORE` under 50%, or a `PLATE-WARN`, means the plate does not read as the region: open the plate beside the crop, name what drifted (subject, framing, palette, style), tighten the prompt with that, and regenerate once. Two misses on one region: keep the better plate, mark it `needs_parent_review`, and say why in one line.
4. Transparent cutouts (a figure or object on the page ground): generate on a flat chroma color absent from the subject and key it to alpha before writing the PNG; never ship the keyed background. 4. Transparent cutouts (a figure or object on the page ground): generate on a flat chroma color absent from the subject and key it to alpha before writing the PNG; never ship the keyed background.
+2 -3
View File
@@ -7,7 +7,6 @@ argument-hint: "[shape · audit|critique · animate|bolder|colorize|delight|layo
license: Apache 2.0 license: Apache 2.0
allowed-tools: allowed-tools:
- Bash(npx impeccable *) - Bash(npx impeccable *)
- Bash(node .claude/skills/impeccable/scripts/*)
--- ---
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft. This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as an award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
@@ -19,7 +18,7 @@ Core principles:
## Setup ## Setup
1. Run `node <skill-base-dir>/scripts/context.mjs` once per session, where `<skill-base-dir>` is the loaded base directory the runtime reports for this skill; keep cwd at the user's project. That base directory resolves every `node .claude/skills/impeccable/scripts/...` command in this skill and its references, and `.claude/skills/impeccable/scripts` is the fallback only when the runtime reports no base directory. Pass a named source file or route as `--target <path>`. It loads PRODUCT.md, DESIGN.md, the matching surface brief, and native-platform guidance when applicable; follow its directives and do not rerun it. 1. Run `node "<skill-base-dir>/scripts/context.mjs"` once per session, where `<skill-base-dir>` is the loaded base directory the runtime reports for this skill; keep cwd at the user's project. Every `node "<skill-base-dir>/scripts/..."` command in this skill and its references resolves against that base directory. Pass a named source file or route as `--target <path>`. It loads PRODUCT.md, DESIGN.md, the matching surface brief, and native-platform guidance when applicable; follow its directives and do not rerun it.
2. Before acting, load the one playbook that owns the request: the Commands table's reference for an explicit or clearly implied sub-command, or [reference/new-work.md](reference/new-work.md) for a new surface or replacement visual world. Then inspect the target and at least one representative source of incumbent visual truth (tokens, theme, CSS, component, or asset) before editing. 2. Before acting, load the one playbook that owns the request: the Commands table's reference for an explicit or clearly implied sub-command, or [reference/new-work.md](reference/new-work.md) for a new surface or replacement visual world. Then inspect the target and at least one representative source of incumbent visual truth (tokens, theme, CSS, component, or asset) before editing.
3. After analysis and direction are resolved, load [reference/craft-floor.md](reference/craft-floor.md) immediately before editing UI. It carries the quality floor, the absolute bans, and the reflexes no detector catches. Do not load it for planning-only work. 3. After analysis and direction are resolved, load [reference/craft-floor.md](reference/craft-floor.md) immediately before editing UI. It carries the quality floor, the absolute bans, and the reflexes no detector catches. Do not load it for planning-only work.
@@ -77,7 +76,7 @@ Routing:
After init writes PRODUCT.md, resume without rerunning `context.mjs`; init loads the native platform reference itself when the platform it recorded is `ios`, `android`, or `adaptive`. After init writes PRODUCT.md, resume without rerunning `context.mjs`; init loads the native platform reference itself when the platform it recorded is `ios`, `android`, or `adaptive`.
**Pin / Unpin:** `node .claude/skills/impeccable/scripts/pin.mjs <pin|unpin> <command>` creates or removes a standalone `/<command>` shortcut. Report the script's result concisely; relay stderr verbatim on error. **Pin / Unpin:** `node "<skill-base-dir>/scripts/pin.mjs" <pin|unpin> <command>` creates or removes a standalone `/<command>` shortcut. Report the script's result concisely; relay stderr verbatim on error.
**Hooks:** `/impeccable hooks <on|off|status|ignore-rule|ignore-file|ignore-value|reset>` manages the design detector hook for this project (auto-runs the detector after UI file edits and surfaces findings). Load [reference/hooks.md](reference/hooks.md) when the user invokes it with any argument. **Hooks:** `/impeccable hooks <on|off|status|ignore-rule|ignore-file|ignore-value|reset>` manages the design detector hook for this project (auto-runs the detector after UI file edits and surfaces findings). Load [reference/hooks.md](reference/hooks.md) when the user invokes it with any argument.
@@ -23,7 +23,7 @@ Resolve one stable target, run two independent assessments, synthesize a design
- "this page" -> the current URL or source file - "this page" -> the current URL or source file
2. **Confirm the target slugs cleanly**: 2. **Confirm the target slugs cleanly**:
```bash ```bash
node .claude/skills/impeccable/scripts/critique-storage.mjs slug "<resolved-path-or-url>" node "<skill-base-dir>/scripts/critique-storage.mjs" slug "<resolved-path-or-url>"
``` ```
Every later command also accepts the resolved target directly and derives the same slug internally; never hand-write a slug. If this exits non-zero, skip persistence and trend for this run, but continue the critique. Every later command also accepts the resolved target directly and derives the same slug internally; never hand-write a slug. If this exits non-zero, skip persistence and trend for this run, but continue the critique.
3. **Read `.impeccable/critique/ignore.md`** if it exists. Drop matching findings silently; it is the only prior-run input critique consumes. 3. **Read `.impeccable/critique/ignore.md`** if it exists. Drop matching findings silently; it is the only prior-run input critique consumes.
@@ -59,7 +59,7 @@ Run the bundled detector and browser visualization evidence. Assessment B is man
CLI scan: CLI scan:
```bash ```bash
node .claude/skills/impeccable/scripts/detect.mjs --json [target] node "<skill-base-dir>/scripts/detect.mjs" --json [target]
``` ```
- Pass markup files/directories as `[target]`; do not pass CSS-only files. - Pass markup files/directories as `[target]`; do not pass CSS-only files.
@@ -73,7 +73,7 @@ Browser visualization is required for a viewable target when browser automation
1. Create a fresh tab and navigate. Prefer the harness's native/browser-canvas screenshot path before hand-rolling a Playwright/Puppeteer script; only fall back to a custom script when no native browser tool is exposed. 1. Create a fresh tab and navigate. Prefer the harness's native/browser-canvas screenshot path before hand-rolling a Playwright/Puppeteer script; only fall back to a custom script when no native browser tool is exposed.
2. Preflight mutable injection by setting `document.title` and appending a `<script>` tag. Read-only evaluate APIs do not count. 2. Preflight mutable injection by setting `document.title` and appending a `<script>` tag. Read-only evaluate APIs do not count.
3. If mutation is unavailable, skip live server, browser presentation, and injection; report fallback signal. 3. If mutation is unavailable, skip live server, browser presentation, and injection; report fallback signal.
4. If mutation is available, start `node .claude/skills/impeccable/scripts/live-server.mjs --background`, present the browser if supported, label `[Human]`, scroll top, inject `http://localhost:PORT/detect.js`, wait 2-3 seconds, read `impeccable` console messages, then stop the live server. 4. If mutation is available, start `node "<skill-base-dir>/scripts/live-server.mjs" --background`, present the browser if supported, label `[Human]`, scroll top, inject `http://localhost:PORT/detect.js`, wait 2-3 seconds, read `impeccable` console messages, then stop the live server.
5. For multi-view targets, inject on 3-5 representative pages. 5. For multi-view targets, inject on 3-5 representative pages.
Return: CLI findings JSON/counts, browser console findings if applicable, false positives, and skipped/failed browser steps with concrete reasons. Return: CLI findings JSON/counts, browser console findings if applicable, false positives, and skipped/failed browser steps with concrete reasons.
@@ -195,7 +195,7 @@ Skip this step if the Setup slug was null (vague or root-level target).
2. **Pass the structured metadata** through `IMPECCABLE_CRITIQUE_META` (JSON), then run the write command: 2. **Pass the structured metadata** through `IMPECCABLE_CRITIQUE_META` (JSON), then run the write command:
```bash ```bash
IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"max_score":<n>,"na_heuristics":"<comma-separated numbers, or empty>","p0_count":<n>,"p1_count":<n>}' \ IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"max_score":<n>,"na_heuristics":"<comma-separated numbers, or empty>","p0_count":<n>,"p1_count":<n>}' \
node .claude/skills/impeccable/scripts/critique-storage.mjs write "<resolved target>" <body-file> node "<skill-base-dir>/scripts/critique-storage.mjs" write "<resolved target>" <body-file>
``` ```
`max_score` is the applicable maximum from the heuristic table (40 when every heuristic applied), so a later run can tell a renormalized total from a full one. The helper prints the absolute path it wrote. `max_score` is the applicable maximum from the heuristic table (40 when every heuristic applied), so a later run can tell a renormalized total from a full one. The helper prints the absolute path it wrote.
@@ -203,7 +203,7 @@ Skip this step if the Setup slug was null (vague or root-level target).
4. **Read the trend** for context: 4. **Read the trend** for context:
```bash ```bash
node .claude/skills/impeccable/scripts/critique-storage.mjs trend "<resolved target>" 5 node "<skill-base-dir>/scripts/critique-storage.mjs" trend "<resolved target>" 5
``` ```
This returns a JSON array of the last 5 frontmatter entries (including the one you just wrote). This returns a JSON array of the last 5 frontmatter entries (including the one you just wrote).
@@ -25,8 +25,8 @@ Every region with `medium: raster` in the spec ships as a plate at its `plate` p
Per region, in the spec's order: Per region, in the spec's order:
1. `node .claude/skills/impeccable/scripts/comp-spec.mjs --crop <id>` writes the reference crop under `.impeccable/build/crops/`. 1. `node "<skill-base-dir>/scripts/comp-spec.mjs" --crop <id>` writes the reference crop under `.impeccable/build/crops/`.
2. Produce the plate. With the API fallback: `node .claude/skills/impeccable/scripts/generate-image.mjs --plate <id> --quality high` does the whole step (crop as reference, the spec's plate prompt, output size chosen from the region's aspect, the file written to its plate path, prompt embedded, and the plate scored against the crop). With a harness-native image tool: use the crop as the input image and `node .claude/skills/impeccable/scripts/comp-spec.mjs --plate-prompt <id>` as the prompt, write the result to the plate path, then run `node .claude/skills/impeccable/scripts/embed-prompt.mjs <plate> --prompt "<the exact prompt>"`. 2. Produce the plate. With the API fallback: `node "<skill-base-dir>/scripts/generate-image.mjs" --plate <id> --quality high` does the whole step (crop as reference, the spec's plate prompt, output size chosen from the region's aspect, the file written to its plate path, prompt embedded, and the plate scored against the crop). With a harness-native image tool: use the crop as the input image and `node "<skill-base-dir>/scripts/comp-spec.mjs" --plate-prompt <id>` as the prompt, write the result to the plate path, then run `node "<skill-base-dir>/scripts/embed-prompt.mjs" <plate> --prompt "<the exact prompt>"`.
3. Read the score line. `PLATE-SCORE` under 50%, or a `PLATE-WARN`, means the plate does not read as the region: open the plate beside the crop, name what drifted (subject, framing, palette, style), tighten the prompt with that, and regenerate once. Two misses on one region: keep the better plate, mark it `needs_parent_review`, and say why in one line. 3. Read the score line. `PLATE-SCORE` under 50%, or a `PLATE-WARN`, means the plate does not read as the region: open the plate beside the crop, name what drifted (subject, framing, palette, style), tighten the prompt with that, and regenerate once. Two misses on one region: keep the better plate, mark it `needs_parent_review`, and say why in one line.
4. Transparent cutouts (a figure or object on the page ground): generate on a flat chroma color absent from the subject and key it to alpha before writing the PNG; never ship the keyed background. 4. Transparent cutouts (a figure or object on the page ground): generate on a flat chroma color absent from the subject and key it to alpha before writing the PNG; never ship the keyed background.
+2 -2
View File
@@ -13,7 +13,7 @@ Three kinds of drift travel under "out of date". Keep them apart:
## Step 1: Run the pass ## Step 1: Run the pass
``` ```
node .claude/skills/impeccable/scripts/doctor.mjs --json node "<skill-base-dir>/scripts/doctor.mjs" --json
``` ```
Add `--target <path>` when the user named a workspace, file, or route in a monorepo. Without it the report describes the repo root, and in a monorepo that is often the wrong project. Add `--target <path>` when the user named a workspace, file, or route in a monorepo. Without it the report describes the repo root, and in a monorepo that is often the wrong project.
@@ -26,7 +26,7 @@ An empty `findings` array is the good outcome. Say so in one line and stop.
The severity says what should happen, not how bad it is. The severity says what should happen, not how bad it is.
- **`auto`** carries no decision. Run `node .claude/skills/impeccable/scripts/doctor.mjs --fix` once to apply these, then report what it moved in one line. Do not ask permission first, and do not ask about them afterward. - **`auto`** carries no decision. Run `node "<skill-base-dir>/scripts/doctor.mjs" --fix` once to apply these, then report what it moved in one line. Do not ask permission first, and do not ask about them afterward.
- **`mention`** needs the user to know but not to decide anything now. State each one in a sentence with its offered fix. - **`mention`** needs the user to know but not to decide anything now. State each one in a sentence with its offered fix.
- **`route`** needs a specific command. Name the command and the gap it would close. Run it only if the user asks in this turn; `init` and `document` are conversations, not repairs you perform unattended. - **`route`** needs a specific command. Name the command and the gap it would close. Run it only if the user asks in this turn; `init` and `document` are conversations, not repairs you perform unattended.
+6 -6
View File
@@ -40,7 +40,7 @@ The first argument is the action. Defaults to `status`.
2. Invoke the admin script and pass the user's output through verbatim: 2. Invoke the admin script and pass the user's output through verbatim:
```bash ```bash
node .claude/skills/impeccable/scripts/hook-admin.mjs <action> [args...] node "<skill-base-dir>/scripts/hook-admin.mjs" <action> [args...]
``` ```
3. If `<action>` is `off`, follow up with a one-line note: "Done. New edits will not trigger the design hook in this project until you run `/impeccable hooks on`." 3. If `<action>` is `off`, follow up with a one-line note: "Done. New edits will not trigger the design hook in this project until you run `/impeccable hooks on`."
@@ -70,32 +70,32 @@ Prefer the narrowest exception:
Example value-specific exception: Example value-specific exception:
```bash ```bash
node .claude/skills/impeccable/scripts/hook-admin.mjs ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional" node "<skill-base-dir>/scripts/hook-admin.mjs" ignore-value overused-font Inter --shared --reason "User confirmed Inter is intentional"
``` ```
Example self-served exception, with the evidence named: Example self-served exception, with the evidence named:
```bash ```bash
node .claude/skills/impeccable/scripts/hook-admin.mjs ignore-value bounce-easing bounce-ball --shared --reason "Agent: literal ball-bounce animation, bounce easing is the subject" node "<skill-base-dir>/scripts/hook-admin.mjs" ignore-value bounce-easing bounce-ball --shared --reason "Agent: literal ball-bounce animation, bounce easing is the subject"
``` ```
Example whole-rule font exception: Example whole-rule font exception:
```bash ```bash
node .claude/skills/impeccable/scripts/hook-admin.mjs ignore-rule overused-font --all-values --reason "User asked to ignore overused fonts generally" node "<skill-base-dir>/scripts/hook-admin.mjs" ignore-rule overused-font --all-values --reason "User asked to ignore overused fonts generally"
``` ```
Example one-rule-in-one-file exception, for a file that is still worth reviewing Example one-rule-in-one-file exception, for a file that is still worth reviewing
for everything else: for everything else:
```bash ```bash
node .claude/skills/impeccable/scripts/hook-admin.mjs ignore-value design-system-font-size "*" --file "src/overlay/widget.js" --reason "Injected widget builds its own type scale; DESIGN.md's ramp describes the site" node "<skill-base-dir>/scripts/hook-admin.mjs" ignore-value design-system-font-size "*" --file "src/overlay/widget.js" --reason "Injected widget builds its own type scale; DESIGN.md's ramp describes the site"
``` ```
Example whole-file exception, for a file that is out of scope entirely: Example whole-file exception, for a file that is out of scope entirely:
```bash ```bash
node .claude/skills/impeccable/scripts/hook-admin.mjs ignore-file "src/legacy/Card.tsx" node "<skill-base-dir>/scripts/hook-admin.mjs" ignore-file "src/legacy/Card.tsx"
``` ```
## Constraints ## Constraints
+1 -1
View File
@@ -25,7 +25,7 @@ When a sub-agent tool is available and permitted, run these independently; other
2. **Mechanical scan:** run: 2. **Mechanical scan:** run:
```bash ```bash
node .claude/skills/impeccable/scripts/detect.mjs --json --scope layout [target files or dirs] node "<skill-base-dir>/scripts/detect.mjs" --json --scope layout [target files or dirs]
``` ```
Also inspect arbitrary spacing, overflow, stacking, and container behavior the detector cannot resolve. Keep mechanical evidence out of the first assessment, then synthesize both passes before editing. A clean scan cannot prove hierarchy or rhythm. Also inspect arbitrary spacing, overflow, stacking, and container behavior the detector cannot resolve. Keep mechanical evidence out of the first assessment, then synthesize both passes before editing. A clean scan cannot prove hierarchy or rhythm.
@@ -45,7 +45,7 @@ On every boot the project is scanned for HTML files under common page roots (`pu
If `config.cspChecked === true`, skip this whole section; the user was already asked once. If `config.cspChecked === true`, skip this whole section; the user was already asked once.
```bash ```bash
node .claude/skills/impeccable/scripts/detect-csp.mjs node "<skill-base-dir>/scripts/detect-csp.mjs"
``` ```
Output `{ shape, signals }`; the shape names the *patch mechanism*, so one template covers many frameworks: Output `{ shape, signals }`; the shape names the *patch mechanism*, so one template covers many frameworks:
+13 -13
View File
@@ -8,7 +8,7 @@ A running dev server with HMR (Vite, Next.js, Bun, etc.), OR a static HTML file
Execute in order. No step skipped, no step reordered. Every tool output in live mode may carry an `_instructions` field: it is the authoritative next step for that exact situation, with real ids and paths substituted; when it conflicts with your recollection of this document, `_instructions` wins. Execute in order. No step skipped, no step reordered. Every tool output in live mode may carry an `_instructions` field: it is the authoritative next step for that exact situation, with real ids and paths substituted; when it conflicts with your recollection of this document, `_instructions` wins.
1. `live.mjs`: boot. If the request names or implies a file, route, or app inside a monorepo, infer the concrete path and run `node .claude/skills/impeccable/scripts/live.mjs --target <path>` instead; then run the rest of this live session from the returned `projectRoot`. The boot resolves the app root from dev-server config files and persists it in `.impeccable/live/roots.json`; every helper re-anchors to that manifest at startup (a wrong cwd cannot fork session state), PRODUCT.md / DESIGN.md are discovered upward to the git root, and relative helper args like `--file` resolve against the app root. 1. `live.mjs`: boot. If the request names or implies a file, route, or app inside a monorepo, infer the concrete path and run `node "<skill-base-dir>/scripts/live.mjs" --target <path>` instead; then run the rest of this live session from the returned `projectRoot`. The boot resolves the app root from dev-server config files and persists it in `.impeccable/live/roots.json`; every helper re-anchors to that manifest at startup (a wrong cwd cannot fork session state), PRODUCT.md / DESIGN.md are discovered upward to the git root, and relative helper args like `--file` resolve against the app root.
2. Open the app URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). Never use `serverPort`; it's the helper, not the app. **Cursor:** `browser_navigate` to that URL before polling; do not skip. **Other harnesses:** use the available browser tool; if the URL is uncertain, ask the user once. 2. Open the app URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). Never use `serverPort`; it's the helper, not the app. **Cursor:** `browser_navigate` to that URL before polling; do not skip. **Other harnesses:** use the available browser tool; if the URL is uncertain, ask the user once.
3. Poll loop with the default long timeout (600000 ms). Run `live-poll.mjs` again immediately after every event or `--reply`; Codex runs this one-shot poll in the foreground. Never pass a short `--timeout=`. The global bar's **Impeccable mark** dims with a pulsing amber dot when nothing is polling `/poll`; restart `live-poll.mjs` to reconnect. 3. Poll loop with the default long timeout (600000 ms). Run `live-poll.mjs` again immediately after every event or `--reply`; Codex runs this one-shot poll in the foreground. Never pass a short `--timeout=`. The global bar's **Impeccable mark** dims with a pulsing amber dot when nothing is polling `/poll`; restart `live-poll.mjs` to reconnect.
4. On `generate`: reuse `event.scaffold` when present; read the screenshot if present; load the action's reference; deliver variants; `--reply done`; poll again. Generate in this thread: you already hold the project's tokens and layout. The overlay preview IS the verification channel; do not screenshot, re-render, or QA variants between generate and accept. Apply craft-floor's contrast, spacing, and type floors by construction as you write; full verification runs once at accept on the chosen variant. 4. On `generate`: reuse `event.scaffold` when present; read the screenshot if present; load the action's reference; deliver variants; `--reply done`; poll again. Generate in this thread: you already hold the project's tokens and layout. The overlay preview IS the verification channel; do not screenshot, re-render, or QA variants between generate and accept. Apply craft-floor's contrast, spacing, and type floors by construction as you write; full verification runs once at accept on the chosen variant.
@@ -31,7 +31,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi
``` ```
LOOP: LOOP:
node .claude/skills/impeccable/scripts/live-poll.mjs # default long timeout; no --timeout= node "<skill-base-dir>/scripts/live-poll.mjs" # default long timeout; no --timeout=
Read JSON; dispatch on "type" Read JSON; dispatch on "type"
"generate" → Handle Generate; reply done; LOOP "generate" → Handle Generate; reply done; LOOP
@@ -52,7 +52,7 @@ LOOP:
## Start ## Start
```bash ```bash
node .claude/skills/impeccable/scripts/live.mjs node "<skill-base-dir>/scripts/live.mjs"
``` ```
Output JSON: `{ ok, serverPort, serverToken, pageFiles, roots, hasProduct, product, productPath, hasDesign, design, designPath, hasSurfaceBrief, surfaceBrief }`. `roots` is the resolved root manifest; `projectRoot` mirrors `roots.appRoot`. The surface brief rides along; do not shell out to `surface-brief.mjs` separately. Precedence for generation: **DESIGN.md wins on visual decisions; PRODUCT.md wins on durable product and voice decisions; the surface brief wins on this surface's strategy.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components (Step 4 Phase A). Identity preservation is the default; departure requires the user's explicit redesign intent. Output JSON: `{ ok, serverPort, serverToken, pageFiles, roots, hasProduct, product, productPath, hasDesign, design, designPath, hasSurfaceBrief, surfaceBrief }`. `roots` is the resolved root manifest; `projectRoot` mirrors `roots.appRoot`. The surface brief rides along; do not shell out to `surface-brief.mjs` separately. Precedence for generation: **DESIGN.md wins on visual decisions; PRODUCT.md wins on durable product and voice decisions; the surface brief wins on this surface's strategy.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components (Step 4 Phase A). Identity preservation is the default; departure requires the user's explicit redesign intent.
@@ -66,9 +66,9 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`,
The append-only journal under `.impeccable/live/sessions/` is canonical durable state (not project source). When the chat was interrupted, polling was missed, the helper restarted, or the browser reloaded: The append-only journal under `.impeccable/live/sessions/` is canonical durable state (not project source). When the chat was interrupted, polling was missed, the helper restarted, or the browser reloaded:
```bash ```bash
node .claude/skills/impeccable/scripts/live-status.mjs # helper state, active sessions, queued events; works with the helper down node "<skill-base-dir>/scripts/live-status.mjs" # helper state, active sessions, queued events; works with the helper down
node .claude/skills/impeccable/scripts/live-resume.mjs --id SESSION_ID # active snapshot, pending event, next safe action node "<skill-base-dir>/scripts/live-resume.mjs" --id SESSION_ID # active snapshot, pending event, next safe action
node .claude/skills/impeccable/scripts/live-complete.mjs --id SESSION_ID # canonical manual final acknowledgement after verified cleanup node "<skill-base-dir>/scripts/live-complete.mjs" --id SESSION_ID # canonical manual final acknowledgement after verified cleanup
``` ```
Server restart rule: start `live-server.mjs` again, then poll; startup requeues unacknowledged events, so never ask the user to click Go again unless `live-resume.mjs` says no active session exists. Server restart rule: start `live-server.mjs` again, then poll; startup requeues unacknowledged events, so never ask the user to click Go again unless `live-resume.mjs` says no active session exists.
@@ -87,7 +87,7 @@ Speed matters; the user is watching the selected element. Reuse preflight metada
2. If `event.scaffold` is present, use it and do **not** run the helper again. Otherwise: 2. If `event.scaffold` is present, use it and do **not** run the helper again. Otherwise:
```bash ```bash
node .claude/skills/impeccable/scripts/live-insert.mjs --id EVENT_ID --count EVENT_COUNT --position after \ node "<skill-base-dir>/scripts/live-insert.mjs" --id EVENT_ID --count EVENT_COUNT --position after \
--element-id "ANCHOR_ID" --classes "class1,class2" --tag "section" --text "ANCHOR_TEXT" --element-id "ANCHOR_ID" --classes "class1,class2" --tag "section" --text "ANCHOR_TEXT"
``` ```
@@ -108,7 +108,7 @@ When `event.scaffold` is present, the helper already found the source and comput
**On source-preview targets `event.scaffold` carries `sourceWritten: false`.** The helper did NOT write the wrapper; it hands you `scaffold.wrapperBlock` plus the picked element's source range (`replaceStartLine`, `replaceEndLine`, 1-indexed). Write the wrapper **and** all variants in ONE edit: splice your variants into `wrapperBlock` at the "Variants: insert below this line" marker, then replace lines `[replaceStartLine, replaceEndLine]` with the result. A separate scaffold write reloads the framework before your variant write lands and strands the browser at 0/N. (`replaceEndLine < replaceStartLine` means insert mode: insert, remove nothing.) The `svelte-component` path never sets `sourceWritten`. **On source-preview targets `event.scaffold` carries `sourceWritten: false`.** The helper did NOT write the wrapper; it hands you `scaffold.wrapperBlock` plus the picked element's source range (`replaceStartLine`, `replaceEndLine`, 1-indexed). Write the wrapper **and** all variants in ONE edit: splice your variants into `wrapperBlock` at the "Variants: insert below this line" marker, then replace lines `[replaceStartLine, replaceEndLine]` with the result. A separate scaffold write reloads the framework before your variant write lands and strands the browser at 0/N. (`replaceEndLine < replaceStartLine` means insert mode: insert, remove nothing.) The `svelte-component` path never sets `sourceWritten`.
```bash ```bash
node .claude/skills/impeccable/scripts/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" --text "TEXT_SNIPPET" node "<skill-base-dir>/scripts/live-wrap.mjs" --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" --text "TEXT_SNIPPET"
``` ```
Flag mapping (keep separate, never collapse into `--query`): `--element-id``event.element.id`; `--classes` ← classes joined with commas; `--tag` ← tagName; `--text` ← first ~80 chars of textContent, **every call**: it disambiguates repeated sibling components, without it wrap lands on the first match. If `event.pageUrl` implies the file, pass `--file PATH`. If `--text` still matches several candidates, wrap exits `{ error: "element_ambiguous", candidates, fallback: "agent-driven" }`: pick the right range from page context and write the wrapper manually per the fallback flow. Flag mapping (keep separate, never collapse into `--query`): `--element-id``event.element.id`; `--classes` ← classes joined with commas; `--tag` ← tagName; `--text` ← first ~80 chars of textContent, **every call**: it disambiguates repeated sibling components, without it wrap lands on the first match. If `event.pageUrl` implies the file, pass `--file PATH`. If `--text` still matches several candidates, wrap exits `{ error: "element_ambiguous", candidates, fallback: "agent-driven" }`: pick the right range from page context and write the wrapper manually per the fallback flow.
@@ -240,14 +240,14 @@ Three kinds: `range` (slider; drives `--p-<id>`; author `var(--p-color-amount, 0
### 8. Signal done ### 8. Signal done
```bash ```bash
node .claude/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH node "<skill-base-dir>/scripts/live-poll.mjs" --reply EVENT_ID done --file RELATIVE_PATH
``` ```
`RELATIVE_PATH` is relative to project root; the browser fetches source directly if the dev server lacks HMR. Then poll again immediately. `RELATIVE_PATH` is relative to project root; the browser fetches source directly if the dev server lacks HMR. Then poll again immediately.
### Aborting an in-flight session ### Aborting an in-flight session
If wrap or generation fails after the browser flipped to GENERATING, tell the **browser** so its bar resets: `node .claude/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID error "Short reason"`. Never use `live-accept --discard` for this (pure file mutator, browser never sees it, bar sticks on dots); `--discard` is only source-side cleanup for a discard the browser itself initiated. If wrap or generation fails after the browser flipped to GENERATING, tell the **browser** so its bar resets: `node "<skill-base-dir>/scripts/live-poll.mjs" --reply EVENT_ID error "Short reason"`. Never use `live-accept --discard` for this (pure file mutator, browser never sees it, bar sticks on dots); `--discard` is only source-side cleanup for a discard the browser itself initiated.
## Handle fallback ## Handle fallback
@@ -288,7 +288,7 @@ Event: `{id, _acceptResult, _completionAck}`. The poll script already restored t
## Handle `steer` ## Handle `steer`
Event: `{id, message, pageUrl}`: page-level direction from the global bar's Steer control (typed or spoken), no element context, no variant cycling. Read `message`, inspect the page or files as needed, make edits or answer in prose. Reply `node .claude/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID steer_done ["Optional short toast"]`, or on failure `--reply EVENT_ID error "Short reason"`, then poll immediately. No separate pickup reply; the Steer bar unlocks on `steer_done` or `error`. Event: `{id, message, pageUrl}`: page-level direction from the global bar's Steer control (typed or spoken), no element context, no variant cycling. Read `message`, inspect the page or files as needed, make edits or answer in prose. Reply `node "<skill-base-dir>/scripts/live-poll.mjs" --reply EVENT_ID steer_done ["Optional short toast"]`, or on failure `--reply EVENT_ID error "Short reason"`, then poll immediately. No separate pickup reply; the Steer bar unlocks on `steer_done` or `error`.
## Handle `prefetch` ## Handle `prefetch`
@@ -304,7 +304,7 @@ When native subagents are available, delegate source edits to `impeccable_manual
If `repair` is present, the previous Apply changed source but final validation failed. Fix the current source and return the same canonical JSON result; do not roll files back yourself. The browser will ask the user before any rollback. If `repair` is present, the previous Apply changed source but final validation failed. Fix the current source and return the same canonical JSON result; do not roll files back yourself. The browser will ask the user before any rollback.
After source edits finish, reply exactly once with `node .claude/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID done --data '{"status":"done","appliedEntryIds":["8hexid"],"failed":[],"files":["src/page.html"],"notes":[]}'`. Use `status:"partial"` or `status:"error"` with `failed[]` when not every entry applied. Then poll again. Never reply without the event id; `--reply done --file ...` is invalid for manual Apply. After source edits finish, reply exactly once with `node "<skill-base-dir>/scripts/live-poll.mjs" --reply EVENT_ID done --data '{"status":"done","appliedEntryIds":["8hexid"],"failed":[],"files":["src/page.html"],"notes":[]}'`. Use `status:"partial"` or `status:"error"` with `failed[]` when not every entry applied. Then poll again. Never reply without the event id; `--reply done --file ...` is invalid for manual Apply.
## Exit ## Exit
@@ -313,7 +313,7 @@ The user stops live mode by saying so in chat, closing the tab (SSE drops; poll
## Cleanup ## Cleanup
```bash ```bash
node .claude/skills/impeccable/scripts/live-server.mjs stop node "<skill-base-dir>/scripts/live-server.mjs" stop
``` ```
Stops the helper and runs `live-inject.mjs --remove` to strip the injected script (use `stop --keep-inject` to keep it for a quick restart; `.impeccable/live/config.json` persists as project config). Then search for and remove any leftover `impeccable-variants-start` wrappers and `impeccable-carbonize-start` blocks. Stops the helper and runs `live-inject.mjs --remove` to strip the injected script (use `stop --keep-inject` to keep it for a quick restart; `.impeccable/live/config.json` persists as project config). Then search for and remove any leftover `impeccable-variants-start` wrappers and `impeccable-carbonize-start` blocks.
+10 -10
View File
@@ -34,7 +34,7 @@ Inherit its world and composition. Resolve only the new purpose, content, hierar
Keep the visual system fixed. Derive five to seven materially different structures from the content, task, and user behavior, ordered by resonance. For a genuinely open whole page, screen, or flow, run: Keep the visual system fixed. Derive five to seven materially different structures from the content, task, and user behavior, ordered by resonance. For a genuinely open whole page, screen, or flow, run:
`node .claude/skills/impeccable/scripts/concept-seed.mjs --scope surface --mode <mode>` `node "<skill-base-dir>/scripts/concept-seed.mjs" --scope surface --mode <mode>`
The script deals three of your structures; the dice pick which three reach the user, breaking the ranking rut while the user keeps a real choice. Present them on the decision page as full cards of equal salience, the dealt lead under kicker THE ROLL, with steer and re-roll; the user locks one. No canon card and no pick card at surface scope: the world is settled, so every card visualizes composition, not identity. With image generation and a comp-led default (`.impeccable/config.json`; the build-path paragraph below), each card declares a `comp` under `.impeccable/mocks/decision/`, generated after serving, in reading order, under [visualize.md](visualize.md)'s comp discipline. Anchor each comp on the established identity: pass a screenshot of a representative existing page as a reference image (the harness image tool's input image, or `generate-image.mjs --ref`) with a prompt that leads with the new surface's structure and names DESIGN.md's palette, type, and component character; prose paraphrases of a design system drift, pixel references do not. Without image generation, or under a code-led default, each card carries a `wireframe` schematic (`serve-question.mjs --schema`) the page draws itself. Locking a card is the approval and sets the build path: a locked comp builds comp-led with that comp as the approved comp, discharging [visualize.md](visualize.md)'s three-option round with no second approval point; a locked wireframe builds code-led, its ambition carried by the direction contract. Never run the script for a local extension or a precisely specified narrow request; shape those directly. The script deals three of your structures; the dice pick which three reach the user, breaking the ranking rut while the user keeps a real choice. Present them on the decision page as full cards of equal salience, the dealt lead under kicker THE ROLL, with steer and re-roll; the user locks one. No canon card and no pick card at surface scope: the world is settled, so every card visualizes composition, not identity. With image generation and a comp-led default (`.impeccable/config.json`; the build-path paragraph below), each card declares a `comp` under `.impeccable/mocks/decision/`, generated after serving, in reading order, under [visualize.md](visualize.md)'s comp discipline. Anchor each comp on the established identity: pass a screenshot of a representative existing page as a reference image (the harness image tool's input image, or `generate-image.mjs --ref`) with a prompt that leads with the new surface's structure and names DESIGN.md's palette, type, and component character; prose paraphrases of a design system drift, pixel references do not. Without image generation, or under a code-led default, each card carries a `wireframe` schematic (`serve-question.mjs --schema`) the page draws itself. Locking a card is the approval and sets the build path: a locked comp builds comp-led with that comp as the approved comp, discharging [visualize.md](visualize.md)'s three-option round with no second approval point; a locked wireframe builds code-led, its ambition carried by the direction contract. Never run the script for a local extension or a precisely specified narrow request; shape those directly.
@@ -43,10 +43,10 @@ The script deals three of your structures; the dice pick which three reach the u
1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; both are the rut, kept out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world. 1. Name the product's unique mechanism in one sentence, the audience's real scene, its cultural home, and what this first surface must prove. Note the page this category always ships and its predictable opposite; both are the rut, kept out of the seven-candidate list. A brief that paints its own picture, a product name, a titled artifact, a governing metaphor, adds its literal reading to the rut: spend at most one candidate on it and derive the rest from elsewhere in the audience's world.
2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily. A nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families. 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, not only its physical objects: the notation, publications, identity programs, data graphics, and interfaces it reads daily. A nameable abstract system (a school of poster, a documentation standard) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; dig until the list spans at least three families.
3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience.
4. Run `node .claude/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode <mode>` and follow what it prints. No substitute, no skip: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure; the roll is what keeps every run from converging on the category default. The script assigns the direction to build and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture list is the point. Close with a verdict per challenger, decided before any borrowing: wins (beats the assigned direction on both axes; becomes the build candidate), competitive (holds one axis; stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a lifted motif is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. 4. Run `node "<skill-base-dir>/scripts/concept-seed.mjs" --scope direction --mode <mode>` and follow what it prints. No substitute, no skip: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure; the roll is what keeps every run from converging on the category default. The script assigns the direction to build and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture list is the point. Close with a verdict per challenger, decided before any borrowing: wins (beats the assigned direction on both axes; becomes the build candidate), competitive (holds one axis; stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes; a lifted motif is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen.
5. Present one direction, fully committed and already raised by the hand it beat, raises visible as named lines: world, first viewport, visitor path, signature interaction, cross-surface reach, honest risk. Route each challenger by verdict: winning and competitive challengers are full alternates with their QUALITY BAR cards and one-line case; declined challengers render demoted, compact and quiet, each carrying its verdict and what the direction kept from it, never full-size, never silently dropped, still adoptable on request. The verdict informs the user's choice, never pre-empts it; the demoted row is the hand's proof of judgment. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker IMPECCABLES PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often where most runs in this category land, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: a lineup of your candidates hands selection back to a taste function and invites the safest card. The pick never takes the lead position; when the dice assign your top candidate there is no pick card, and the assigned card notes it topped your list. Add re-roll with an optional one-line steer, in three registers: plain (a fresh hand, same spread), safer (your remaining conventional grounded candidates plus the canon against named competitors), bolder (foreign forms only, at full commitment). The register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register <value>` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool, whose option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit last; declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel. 5. Present one direction, fully committed and already raised by the hand it beat, raises visible as named lines: world, first viewport, visitor path, signature interaction, cross-surface reach, honest risk. Route each challenger by verdict: winning and competitive challengers are full alternates with their QUALITY BAR cards and one-line case; declined challengers render demoted, compact and quiet, each carrying its verdict and what the direction kept from it, never full-size, never silently dropped, still adoptable on request. The verdict informs the user's choice, never pre-empts it; the demoted row is the hand's proof of judgment. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker IMPECCABLES PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often where most runs in this category land, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: a lineup of your candidates hands selection back to a taste function and invites the safest card. The pick never takes the lead position; when the dice assign your top candidate there is no pick card, and the assigned card notes it topped your list. Add re-roll with an optional one-line steer, in three registers: plain (a fresh hand, same spread), safer (your remaining conventional grounded candidates plus the canon against named competitors), bolder (foreign forms only, at full commitment). The register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register <value>` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool, whose option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit last; declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel.
The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it (the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path), convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. Record a standing preference as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. Re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, its raised lines included; the pick card when one exists; the dealt challengers as alternates with their QUALITY BAR cards, verdicts, and kept lines; re-roll with its safer and bolder registers; steer; canon enabled; and `buildPath` carrying the recorded default with `toggle: true` whenever image generation exists (details in the build-path paragraph below). A degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy: thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (`--schema` prints the exact shape); the page renders identity from these fields, demotes declined challengers to their row on its own, and a challenger's catalog image rides as labeled inspiration, never the promise of the build. Author `canonCard` too: the category standard as one honest card, same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node .claude/skills/impeccable/scripts/serve-question.mjs --start --payload <file>` (`--schema` first for the payload shape). It daemonizes, prints the page URL and a key, and exits; open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key <key>`, repeating while it exits 3; the ANSWER prints as JSON. An ANSWER of `{"optionId":"reroll"}` keeps the server alive and the page open on a loading hand: rerun concept-seed with the same `--scope` and `--mode` plus `--from <seed-key> --reroll <n>` (1 on the first re-roll, counting up), build the next payload, deliver it with `--update --key <same key> --payload <file>`, then return to `--wait` on that key. Never `--start` a second server or fall back to chat here: either strands the open tab on a hand that never arrives. Exit 4 means the page closed unanswered: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may run the script without `--start` and let it auto-open and block. Never predict the fallback: run the script, and only exit code 2 from starting it routes the decision to the structured tool; that exit is the fallback, never an error to retry. The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it (the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path), convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. Record a standing preference as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. Re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, its raised lines included; the pick card when one exists; the dealt challengers as alternates with their QUALITY BAR cards, verdicts, and kept lines; re-roll with its safer and bolder registers; steer; canon enabled; and `buildPath` carrying the recorded default with `toggle: true` whenever image generation exists (details in the build-path paragraph below). A degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy: thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (`--schema` prints the exact shape); the page renders identity from these fields, demotes declined challengers to their row on its own, and a challenger's catalog image rides as labeled inspiration, never the promise of the build. Author `canonCard` too: the category standard as one honest card, same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `node "<skill-base-dir>/scripts/serve-question.mjs" --start --payload <file>` (`--schema` first for the payload shape). It daemonizes, prints the page URL and a key, and exits; open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key <key>`, repeating while it exits 3; the ANSWER prints as JSON. An ANSWER of `{"optionId":"reroll"}` keeps the server alive and the page open on a loading hand: rerun concept-seed with the same `--scope` and `--mode` plus `--from <seed-key> --reroll <n>` (1 on the first re-roll, counting up), build the next payload, deliver it with `--update --key <same key> --payload <file>`, then return to `--wait` on that key. Never `--start` a second server or fall back to chat here: either strands the open tab on a hand that never arrives. Exit 4 means the page closed unanswered: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may run the script without `--start` and let it auto-open and block. Never predict the fallback: run the script, and only exit code 2 from starting it routes the decision to the structured tool; that exit is the fallback, never an error to retry.
When image generation exists, every card also declares a `comp` path under `.impeccable/mocks/decision/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the comps; the page shimmer-waits per slot and the user may answer before they land. Each card's image is that direction's north-star comp at full fidelity under [visualize.md](visualize.md)'s comp discipline: the requested surface's first viewport, structure-led prompt, real product name and real content, no invented commercial claims, in that card's own palette, type character, and material world, committed all the way. Generation takes the same time at any fidelity, so an unfinished draft pays comp cost for draft quality; fairness between cards is equal fidelity in each card's own grammar, one surface, one aspect, never shared unfinishedness. The frame's aspect is the surface's own: portrait at device viewport for a native app or mobile-first surface, landscape for desktop web; the decision page adapts to either, and a phone screen comped landscape is a broken frame, not a neutral default. Produce in reading order, the assigned card, then the pick, then the full-card hand, then canon, each file written with its prompt sidecar the moment it is done, so a re-roll's spend front-loads onto the cards read first; declined challengers get no comp, their catalog thumb is their face. With parallel subagents, fan out one agent per card: each spawn is the shipped asset producer with a single-comp packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight. Regenerate inline any slot still empty when its agent returns; drop without ceremony any slot still empty when the user answers. No other supervision is owed. Without parallel subagents, generate in the main thread after serving, same order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. The chosen card's comp is not spent by the choice: comp-led, it enters the comp round as compositional option one; code-led, it returns at the finish review as the critique reference, what the image dared that the build did not. Unchosen comps stay in `.impeccable/mocks/decision/` as the round's spent hand; they carry no approval and imply none. With no image generation, cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. When image generation exists, every card also declares a `comp` path under `.impeccable/mocks/decision/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the comps; the page shimmer-waits per slot and the user may answer before they land. Each card's image is that direction's north-star comp at full fidelity under [visualize.md](visualize.md)'s comp discipline: the requested surface's first viewport, structure-led prompt, real product name and real content, no invented commercial claims, in that card's own palette, type character, and material world, committed all the way. Generation takes the same time at any fidelity, so an unfinished draft pays comp cost for draft quality; fairness between cards is equal fidelity in each card's own grammar, one surface, one aspect, never shared unfinishedness. The frame's aspect is the surface's own: portrait at device viewport for a native app or mobile-first surface, landscape for desktop web; the decision page adapts to either, and a phone screen comped landscape is a broken frame, not a neutral default. Produce in reading order, the assigned card, then the pick, then the full-card hand, then canon, each file written with its prompt sidecar the moment it is done, so a re-roll's spend front-loads onto the cards read first; declined challengers get no comp, their catalog thumb is their face. With parallel subagents, fan out one agent per card: each spawn is the shipped asset producer with a single-comp packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight. Regenerate inline any slot still empty when its agent returns; drop without ceremony any slot still empty when the user answers. No other supervision is owed. Without parallel subagents, generate in the main thread after serving, same order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. The chosen card's comp is not spent by the choice: comp-led, it enters the comp round as compositional option one; code-led, it returns at the finish review as the critique reference, what the image dared that the build did not. Unchosen comps stay in `.impeccable/mocks/decision/` as the round's spent hand; they carry no approval and imply none. With no image generation, cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images.
@@ -78,9 +78,9 @@ On a new or replacement world, DESIGN.md is written at finish, from the built wo
Read the existing surface brief before updating it: Read the existing surface brief before updating it:
`node .claude/skills/impeccable/scripts/surface-brief.mjs read <primary-target>` `node "<skill-base-dir>/scripts/surface-brief.mjs" read <primary-target>`
`node .claude/skills/impeccable/scripts/surface-brief.mjs write <primary-target> <body-file> [related-target ...]` `node "<skill-base-dir>/scripts/surface-brief.mjs" write <primary-target> <body-file> [related-target ...]`
After writing, read the brief once more and verify that all six contract blocks and the seed key are present before building. After writing, read the brief once more and verify that all six contract blocks and the seed key are present before building.
@@ -98,9 +98,9 @@ Build the assigned direction, not a safer interpretation of it. The form supplie
When an approved comp exists, it is a spatial contract, not a mood board: only the user can downgrade its authority, in explicit words. Models systematically believe their HTML, CSS, and SVG recreation of an image succeeded when it did not, so the build runs as a state machine on disk whose gates measure the screen against the comp instead of asking you to remember it. Start it once, and let it tell you what is next: When an approved comp exists, it is a spatial contract, not a mood board: only the user can downgrade its authority, in explicit words. Models systematically believe their HTML, CSS, and SVG recreation of an image succeeded when it did not, so the build runs as a state machine on disk whose gates measure the screen against the comp instead of asking you to remember it. Start it once, and let it tell you what is next:
`node .claude/skills/impeccable/scripts/build-phase.mjs start --direction <seed key> --kind <assigned|pick|challenger|canon>` right after the direction choice (this is also the choice ping; the roll's output names the exact command), or `start --comp <approved comp>` when a surface round already locked one. `node "<skill-base-dir>/scripts/build-phase.mjs" start --direction <seed key> --kind <assigned|pick|challenger|canon>` right after the direction choice (this is also the choice ping; the roll's output names the exact command), or `start --comp <approved comp>` when a surface round already locked one.
Then, in order, each closed by `node .claude/skills/impeccable/scripts/build-phase.mjs advance` (every script below lives under `.claude/skills/impeccable/scripts/` and runs with `node`; exit 2 means the gate failed and printed why; fix that and advance again; write nothing for a later phase while an earlier gate is open): Then, in order, each closed by `node "<skill-base-dir>/scripts/build-phase.mjs" advance` (every script below lives under `<skill-base-dir>/scripts/` and runs with `node`; exit 2 means the gate failed and printed why; fix that and advance again; write nothing for a later phase while an earlier gate is open):
0. **comps.** The comp round from [visualize.md](visualize.md): three compositional comps of the requested surface at its own viewport under `.impeccable/mocks/`, each with a prompt sidecar, put in front of the user; the chosen one's sidecar gets `"approved": true`. The gate counts them and reads the approval; a `start --comp` skips this phase because it already happened. 0. **comps.** The comp round from [visualize.md](visualize.md): three compositional comps of the requested surface at its own viewport under `.impeccable/mocks/`, each with a prompt sidecar, put in front of the user; the chosen one's sidecar gets `"approved": true`. The gate counts them and reads the approval; a `start --comp` skips this phase because it already happened.
The comp-led path is a frontier-tier job: it asks the builder to hold a measured layout, place plates at their boxes, and act on numeric readings across a dozen attempts. Smaller or faster models produce a recognisable page and stall under the hero gate; if the model in hand is one of those, say so before the direction round and take the code-led path, or expect the run to end at the hero with its readings unmet. The comp-led path is a frontier-tier job: it asks the builder to hold a measured layout, place plates at their boxes, and act on numeric readings across a dozen attempts. Smaller or faster models produce a recognisable page and stall under the hero gate; if the model in hand is one of those, say so before the direction round and take the code-led path, or expect the run to end at the hero with its readings unmet.
@@ -130,17 +130,17 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi
## 7. Inspect and finish ## 7. Inspect and finish
Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. When the harness reports the user's actual viewport (an in-app browser's size, a named resolution), add that width to the set: the width that breaks is the one the user sees first. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. On a comp-led build, run `node .claude/skills/impeccable/scripts/comp-diff.mjs --comp <approved comp> --build .impeccable/review/desktop.png --spec .impeccable/build/spec.json --out-dir .impeccable/review/diff/final` and read its region rows and paired crops as the critique: the side-by-side is the view the build thread never has on its own, and a region it scores missing or contradicted is a fix whatever the page looks like from memory. Never judge fidelity from one full-page thumbnail; it hides exactly the failures that matter. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary. Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. When the harness reports the user's actual viewport (an in-app browser's size, a named resolution), add that width to the set: the width that breaks is the one the user sees first. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. On a comp-led build, run `node "<skill-base-dir>/scripts/comp-diff.mjs" --comp <approved comp> --build .impeccable/review/desktop.png --spec .impeccable/build/spec.json --out-dir .impeccable/review/diff/final` and read its region rows and paired crops as the critique: the side-by-side is the view the build thread never has on its own, and a region it scores missing or contradicted is a fix whatever the page looks like from memory. Never judge fidelity from one full-page thumbnail; it hides exactly the failures that matter. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary.
A capture is evidence only when it is valid, and you validate before you send. Settle or disable entrance motion first: an element hidden by animation timing reads as a missing element and gets fixed into a regression. Capture full-page shots from the document top. Capture the comp comparison at the comp's own pixel dimensions. Then open every file once and confirm it shows what its name claims: no black or blank regions, no wrong section behind a right filename, no half-loaded state. A malformed capture sent onward costs the whole round; the reviewer answers it with `disposition: recapture` and nothing it reviewed binds. A capture is evidence only when it is valid, and you validate before you send. Settle or disable entrance motion first: an element hidden by animation timing reads as a missing element and gets fixed into a regression. Capture full-page shots from the document top. Capture the comp comparison at the comp's own pixel dimensions. Then open every file once and confirm it shows what its name claims: no black or blank regions, no wrong section behind a right filename, no half-loaded state. A malformed capture sent onward costs the whole round; the reviewer answers it with `disposition: recapture` and nothing it reviewed binds.
After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node .claude/skills/impeccable/scripts/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`, plus `user-<width>.png` whenever the user's viewport joined the inspected set; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, every viewport you inspected is named required in the packet, and that directory is where it looks when a passed path is missing. After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node "<skill-base-dir>/scripts/detect.mjs" --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`, plus `user-<width>.png` whenever the user's viewport joined the inspected set; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, every viewport you inspected is named required in the packet, and that directory is where it looks when a passed path is missing.
Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, the direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (a code-led build has no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), on a comp-led build the build state (`.impeccable/build/state.json`), the spec, and the diff directories (`.impeccable/review/diff/hero/` and `.impeccable/review/diff/final/`, whose side-by-side, heatmap, region pairs, and `report.json` are the fidelity evidence), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify the return carries the five contract sections (a recapture return carries one, its recapture list); on an empty or thrashed return, respawn once with the same inputs. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness with no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex; `/impeccable-finish-reviewer` in Cursor; on GitHub Copilot say "Use the impeccable-finish-reviewer agent"), with the original request, confirmed answers, the artifact path, the screenshot paths, the direction contract, existing hook findings, the QUALITY BAR card and approved comp paths (a code-led build has no approved comp; the chosen decision comp rides in that slot as the critique reference, named as such), on a comp-led build the build state (`.impeccable/build/state.json`), the spec, and the diff directories (`.impeccable/review/diff/hero/` and `.impeccable/review/diff/final/`, whose side-by-side, heatmap, region pairs, and `report.json` are the fidelity evidence), the craft-floor reference path, and on a native platform the platform reference path(s), [ios.md](ios.md) / [android.md](android.md), both on adaptive, plus one line saying no detector ran, so the reviewer judges in the platform's conventions rather than the web's. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Never read the shipped agents' definition files before spawning; the harness loads them at spawn, and you owe only the input packet. Wait on any agent with one long timeout rather than a loop of short polls, and spend the wait on the next independent step. Verify the return carries the five contract sections (a recapture return carries one, its recapture list); on an empty or thrashed return, respawn once with the same inputs. This review never runs inside the build thread and never inherits it: spawn the reviewer fresh, with no forked conversation history (`fork_turns: 0` in codex); a reviewer that inherits your transcript inherits your framing, your optimism, and your abstractions, and everything it needs travels in the inputs above. Only a harness with no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently.
Act on the disposition word; there are exactly four. **recapture**: the evidence failed, not the build. Recapture what the return names under the capture-validity rules, then run a full review over the new evidence. A review conducted on invalid evidence binds nothing, and a verdict pass may never follow it. **rebuild**: fidelity failed wholesale, not in patches. Skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a fresh full review, never a verdict pass; a rebuild replaces regions wholesale, so the whole matrix runs again over the recaptures. Tell the user what is happening rather than asking permission to fix a failure. Consult the user only on a second rebuild directive, both verdicts on the table, or when rebuilding would discard content the user approved. **ship**: nothing is owed; report the verdict at its scope and continue to the documenter. **fix**: apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever decides, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Do not run a second detector. Act on the disposition word; there are exactly four. **recapture**: the evidence failed, not the build. Recapture what the return names under the capture-validity rules, then run a full review over the new evidence. A review conducted on invalid evidence binds nothing, and a verdict pass may never follow it. **rebuild**: fidelity failed wholesale, not in patches. Skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a fresh full review, never a verdict pass; a rebuild replaces regions wholesale, so the whole matrix runs again over the recaptures. Tell the user what is happening rather than asking permission to fix a failure. Consult the user only on a second rebuild directive, both verdicts on the table, or when rebuilding would discard content the user approved. **ship**: nothing is owed; report the verdict at its scope and continue to the documenter. **fix**: apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever decides, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Do not run a second detector.
A rebuild and a fix round share one asset rule: a raster either round creates or replaces is still asset work under [visualize.md](visualize.md)'s Produce section and keeps its **provenance** like every build raster, and a raster the round abandons is deleted in the same batch. Before either round's result goes back for review or verdict, run `node .claude/skills/impeccable/scripts/embed-prompt.mjs --scan <asset-dir...>` over the directories the artifact's rasters ship from and clear every file it reports by embedding what it is missing: the exact generation prompt for a produced raster, the origin for a sourced, stock, or pre-existing one. The scan only reads; deletion is reserved for rasters the round abandoned, never for a file the scan flagged. A rebuild and a fix round share one asset rule: a raster either round creates or replaces is still asset work under [visualize.md](visualize.md)'s Produce section and keeps its **provenance** like every build raster, and a raster the round abandons is deleted in the same batch. Before either round's result goes back for review or verdict, run `node "<skill-base-dir>/scripts/embed-prompt.mjs" --scan <asset-dir...>` over the directories the artifact's rasters ship from and clear every file it reports by embedding what it is missing: the exact generation prompt for a produced raster, the origin for a sourced, stock, or pre-existing one. The scan only reads; deletion is reserved for rasters the round abandoned, never for a file the scan flagged.
Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice.
+1 -1
View File
@@ -29,7 +29,7 @@ Use the feature yourself at the surface's representative sizes: desktop and mobi
If a prior critique exists, use it as one input: If a prior critique exists, use it as one input:
```bash ```bash
node .claude/skills/impeccable/scripts/critique-storage.mjs latest "<resolved target>" node "<skill-base-dir>/scripts/critique-storage.mjs" latest "<resolved target>"
``` ```
Exit 0 returns the latest snapshot; incorporate relevant P0/P1 findings and name the snapshot read. Exit 2 means none exists. Perform an independent pass either way. Exit 0 returns the latest snapshot; incorporate relevant P0/P1 findings and name the snapshot read. Exit 2 means none exists. Perform an independent pass either way.
@@ -2,7 +2,7 @@
Read this when the user invokes `/impeccable` with no argument. They are asking "what should I do?" Make the menu context-aware instead of static. Read this when the user invokes `/impeccable` with no argument. They are asking "what should I do?" Make the menu context-aware instead of static.
Setup has already run `context.mjs`. If that reported `NO_PRODUCT_MD`, the project has no captured context yet: lead the menu with `/impeccable init` as the top recommendation (one line on why) and still show the rest below; don't silently jump into init. Otherwise run `node .claude/skills/impeccable/scripts/context-signals.mjs` once and read its JSON, then lead with the **2-3 highest-value next commands**, each with a one-line reason pulled from the signals, followed by the full menu (the Commands table in SKILL.md, grouped by category). **Never auto-run a command; the recommendation is a suggestion the user confirms.** Setup has already run `context.mjs`. If that reported `NO_PRODUCT_MD`, the project has no captured context yet: lead the menu with `/impeccable init` as the top recommendation (one line on why) and still show the rest below; don't silently jump into init. Otherwise run `node "<skill-base-dir>/scripts/context-signals.mjs"` once and read its JSON, then lead with the **2-3 highest-value next commands**, each with a one-line reason pulled from the signals, followed by the full menu (the Commands table in SKILL.md, grouped by category). **Never auto-run a command; the recommendation is a suggestion the user confirms.**
Reason over the signals; there is no score to obey: Reason over the signals; there is no score to obey:
@@ -13,6 +13,6 @@ Reason over the signals; there is no score to obey:
- `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. **`live` and the bundled `detect.mjs` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with either; the browser overlay and the HTML rule engine don't apply to native app code. - `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. **`live` and the bundled `detect.mjs` are web-only.** If `setup.platform` is `ios`, `android`, or `adaptive`, don't lead with either; the browser overlay and the HTML rule engine don't apply to native app code.
- Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`. - Otherwise group by intent (build new / improve what's there / iterate visually), tailored to the current surface and `setup.platform`.
**If `scan.targets` is non-empty and `setup.platform` is not `ios`/`android`/`adaptive`, run `node .claude/skills/impeccable/scripts/detect.mjs --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx; it reads HTML/CSS, so skip it for native projects). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it. **If `scan.targets` is non-empty and `setup.platform` is not `ios`/`android`/`adaptive`, run `node "<skill-base-dir>/scripts/detect.mjs" --json <scan.targets joined by spaces>` once** (the bundled detector over local files: no network, no npx; it reads HTML/CSS, so skip it for native projects). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it.
Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede. Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede.
@@ -24,7 +24,7 @@ When a sub-agent tool is available and permitted, run these independently; other
2. **Mechanical scan:** run: 2. **Mechanical scan:** run:
```bash ```bash
node .claude/skills/impeccable/scripts/detect.mjs --json --scope type [target files or dirs] node "<skill-base-dir>/scripts/detect.mjs" --json --scope type [target files or dirs]
``` ```
Also inspect dynamic or arbitrary font values the detector cannot interpret. Synthesize both assessments before editing, noting what each caught alone. A clean scan is a floor, not proof of good typography. Also inspect dynamic or arbitrary font values the detector cannot interpret. Synthesize both assessments before editing, noting what each caught alone. A clean scan is a floor, not proof of good typography.
@@ -39,7 +39,7 @@ What the comp shows is measured, not remembered. new-work.md section 6 runs the
## Plates and provenance ## Plates and provenance
Every raster region's plate is produced in the plates phase, before any page code, by the shipped asset producer or in the current thread (`generate-image.mjs --plate <id>`, or the harness image tool with the crop as input and the spec's plate prompt). Generation context is part of the asset: after generating any image with any tool, run `node .claude/skills/impeccable/scripts/embed-prompt.mjs <image> --prompt "<prompt>"` with the exact string the tool received (`generate-image.mjs` does this itself), so the intent lives inside the file; `--read` recovers it, `--scan <dir>` lists rasters still missing one. The embedded prompt plus the region's row in the spec is the raster's **provenance**, and every raster the artifact references carries it; a sourced, stock, or pre-existing raster embeds its origin instead. A raster created or replaced later, in a fix batch or a reviewer's rebuild, is produced the same way; a raster a fix abandons is deleted in the same batch. Every raster region's plate is produced in the plates phase, before any page code, by the shipped asset producer or in the current thread (`generate-image.mjs --plate <id>`, or the harness image tool with the crop as input and the spec's plate prompt). Generation context is part of the asset: after generating any image with any tool, run `node "<skill-base-dir>/scripts/embed-prompt.mjs" <image> --prompt "<prompt>"` with the exact string the tool received (`generate-image.mjs` does this itself), so the intent lives inside the file; `--read` recovers it, `--scan <dir>` lists rasters still missing one. The embedded prompt plus the region's row in the spec is the raster's **provenance**, and every raster the artifact references carries it; a sourced, stock, or pre-existing raster embeds its origin instead. A raster created or replaced later, in a fix batch or a reviewer's rebuild, is produced the same way; a raster a fix abandons is deleted in the same batch.
Convert images with a converter context.mjs reported at boot (the IMAGE_TOOLS line); probe only when it reported none, at most once per session, never per image. Convert images with a converter context.mjs reported at boot (the IMAGE_TOOLS line); probe only when it reported none, at most once per session, never per image.
+21
View File
@@ -25,6 +25,12 @@ import { hooksJsonFor, buildClaudePluginHooksManifest } from './lib/transformers
import { createAllZips, createProviderZip } from './lib/zip.js'; import { createAllZips, createProviderZip } from './lib/zip.js';
import { collectPluginVersions } from './lib/validate-plugin-versions.js'; import { collectPluginVersions } from './lib/validate-plugin-versions.js';
import { collectPluginManifestFindings } from './lib/validate-plugin-manifest.js'; import { collectPluginManifestFindings } from './lib/validate-plugin-manifest.js';
import {
rewritePluginMarkdownTree,
rewritePluginAgentMarkdown,
verifyPluginSkillRewrite,
verifyPluginAgentRewrite,
} from './lib/plugin-paths.js';
import { stageOpenAIPlugin } from './lib/openai-plugin.js'; import { stageOpenAIPlugin } from './lib/openai-plugin.js';
import { ANTIPATTERNS } from '../cli/engine/registry/antipatterns.mjs'; import { ANTIPATTERNS } from '../cli/engine/registry/antipatterns.mjs';
// Sub-page generation is now handled by Astro content collections. // Sub-page generation is now handled by Astro content collections.
@@ -751,6 +757,21 @@ async function build() {
copyDirSync(claudeAgentsSrc, pluginAgentsDir); copyDirSync(claudeAgentsSrc, pluginAgentsDir);
} }
// The claude-code output resolves {{scripts_path}} to a project-relative
// path. Inside the plugin cache that path points into the user's project,
// so a dual install silently runs the project's older skill copy (issue
// #523). Rewrite the copied markdown to the skill-base-dir form.
rewritePluginMarkdownTree(pluginSkillsDir);
// Agents get the plugin-root variable, not the skill-base-dir token:
// a spawned agent never loads SKILL.md, so the token is undefined there.
rewritePluginMarkdownTree(pluginAgentsDir, rewritePluginAgentMarkdown);
verifyPluginSkillRewrite(path.join(pluginSkillsDir, 'impeccable', 'SKILL.md'));
if (fs.existsSync(pluginAgentsDir)) {
for (const agentFile of fs.readdirSync(pluginAgentsDir)) {
if (agentFile.endsWith('.md')) verifyPluginAgentRewrite(path.join(pluginAgentsDir, agentFile));
}
}
// Ship the design detector as a plugin-packaged hook. Claude Code and // Ship the design detector as a plugin-packaged hook. Claude Code and
// Grok Build both auto-discover `hooks/hooks.json` at the plugin root // Grok Build both auto-discover `hooks/hooks.json` at the plugin root
// (Grok aliases CLAUDE_PLUGIN_ROOT → GROK_PLUGIN_ROOT), so marketplace / // (Grok aliases CLAUDE_PLUGIN_ROOT → GROK_PLUGIN_ROOT), so marketplace /
+189
View File
@@ -0,0 +1,189 @@
import fs from 'fs';
import path from 'path';
/**
* Rewrite project-relative script paths for the plugin subtree (issue #523).
*
* The ./plugin subtree is a verbatim copy of the dist/claude-code output,
* where {{scripts_path}} resolves to `.claude/skills/impeccable/scripts`,
* a path relative to the user's project. Run from the plugin cache, that
* path points at whatever the project has installed: a plugin-only user
* gets MODULE_NOT_FOUND, and a dual-install user silently runs the
* project's (possibly older) skill copy.
*
* No literal path survives installation (the plugin cache location varies
* per machine and per plugin version), so skill and reference markdown
* uses the `<skill-base-dir>` form SKILL.md's Setup step 1 already leads
* with: the runtime shows the skill's loaded base directory when it loads
* the skill, and scripts resolve against that. Agent files cannot use the
* token (a spawned agent never loads SKILL.md) and get the
* ${CLAUDE_PLUGIN_ROOT} variable instead; see PLUGIN_AGENT_SCRIPTS_PATH.
*/
// The resolved {{scripts_path}} in dist/claude-code output, fixed by the
// Claude Code transformer's configDir (.claude) + skill name (impeccable).
export const CLAUDE_PROJECT_SCRIPTS_PATH = '.claude/skills/impeccable/scripts';
export const PLUGIN_SCRIPTS_PATH = '<skill-base-dir>/scripts';
// The project-path rule pre-approves a path inside the user's project, the
// one place the plugin must NOT run scripts from. No replacement rule
// exists: a wildcard pattern such as `node */skills/impeccable/scripts/*`
// auto-approves any same-shaped path anywhere on disk, and frontmatter has
// no variable bound to the loaded plugin root (CLAUDE_PLUGIN_ROOT is
// hook-only). The plugin copy drops the rule and script runs go through
// the normal Bash confirmation.
export const PROJECT_ALLOWED_TOOLS_LINE = ` - Bash(node ${CLAUDE_PROJECT_SCRIPTS_PATH}/*)\n`;
// Setup step 1's second sentence names the project path as the fallback
// when the runtime reports no base directory. A plugin install has no
// working project fallback (that path is the bug this rewrite exists to
// fix), and every instruction in the plugin copy already carries the
// token, so the sentence loses its fallback clause.
const SETUP_FALLBACK_TEXT =
'That base directory resolves every `node .claude/skills/impeccable/scripts/...` command in this skill and its references, ' +
'and `.claude/skills/impeccable/scripts` is the fallback only when the runtime reports no base directory.';
const SETUP_PLUGIN_TEXT =
'Every `node "<skill-base-dir>/scripts/..."` command in this skill and its references resolves against that base directory.';
// Agent files are subagent system prompts: a spawned agent never loads
// SKILL.md, so Setup's <skill-base-dir> token is undefined in the one
// context that must act on it (review finding). Claude Code substitutes
// ${CLAUDE_PLUGIN_ROOT} inline anywhere in plugin skill and agent content
// per the substitution table in code.claude.com/docs/en/plugins-reference.
// (anthropics/claude-code#65768 observed subagents receiving the literal;
// it was auto-closed stale and the docs table postdates it. Frontmatter
// still has no variable, which is why the node pre-approval is dropped
// rather than rewritten.) Grok Build reads this same subtree with its own
// substitution behavior, so the embed instruction carries a sidecar
// fallback for any harness that hands the agent the unexpanded literal.
export const PLUGIN_AGENT_SCRIPTS_PATH = '${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts';
// Appended as its own sentence after the agent's embed instruction so
// behavior is defined even where the variable reaches the agent
// unexpanded: the prompt survives as a sidecar and the manifest tells the
// parent, whose own thread can resolve the script and embed it properly.
export const AGENT_EMBED_FALLBACK =
' When that script path is unreachable in your environment, write the same prompt to ' +
'`<asset>.prompt.txt` beside the asset and note it in your manifest so the parent can embed it.';
/**
* Rewrite one markdown file's content for the plugin subtree. Pure, so the
* unit suite can pin every rewrite without a build.
*/
export function rewritePluginMarkdown(content) {
return content
// Order matters: the allowed-tools line contains the project path, so
// remove it before the generic path replacement rewrites it into a
// line the removal no longer matches.
.replaceAll(PROJECT_ALLOWED_TOOLS_LINE, '')
.replaceAll(SETUP_FALLBACK_TEXT, SETUP_PLUGIN_TEXT)
.replaceAll(CLAUDE_PROJECT_SCRIPTS_PATH, PLUGIN_SCRIPTS_PATH)
// <skill-base-dir> expands to a real path at run time, and an unquoted
// path with spaces splits before node sees it. Quote every command's
// script argument, including the token-form commands SKILL.src.md
// carries natively (Setup step 1). Runs after the path replacement so
// one pattern covers both origins; already-quoted forms don't match.
.replace(/node <skill-base-dir>\/scripts\/([^\s`"]+)/g, 'node "<skill-base-dir>/scripts/$1"');
}
/**
* Rewrite one agent file's content for the plugin subtree. Same quoting
* discipline as the skill rewrite, but the path is the plugin-root
* variable rather than the skill-base-dir token SKILL.md defines,
* because no SKILL.md travels with a spawned agent.
*/
export function rewritePluginAgentMarkdown(content) {
return content
.replaceAll(CLAUDE_PROJECT_SCRIPTS_PATH, PLUGIN_AGENT_SCRIPTS_PATH)
.replace(
/node \$\{CLAUDE_PLUGIN_ROOT\}\/skills\/impeccable\/scripts\/([^\s`"]+)/g,
'node "${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/$1"',
)
// Anchors on the command this rewrite just produced plus the rest of
// its sentence, so the fallback lands as the following sentence rather
// than splicing into the middle of one.
.replace(
/(`node "\$\{CLAUDE_PLUGIN_ROOT\}\/skills\/impeccable\/scripts\/embed-prompt\.mjs"[^`]*`[^.]*\.)/g,
`$1${AGENT_EMBED_FALLBACK}`,
);
}
/**
* Fail the build when an agent file's rewrite no longer holds: a
* project-relative scripts path or the skill-base-dir token survived, or
* an embed instruction lost its unexpanded-variable fallback because the
* source sentence the anchor keys on was reworded. Loud beats a silent
* no-op, same contract as verifyPluginSkillRewrite.
*/
export function verifyPluginAgentRewrite(agentPath) {
const content = fs.readFileSync(agentPath, 'utf-8');
if (content.includes(CLAUDE_PROJECT_SCRIPTS_PATH) || content.includes('<skill-base-dir>')) {
throw new Error(
`Plugin rewrite drift: ${agentPath} references a scripts path a spawned agent cannot ` +
'resolve (the project-relative form or the <skill-base-dir> token). Agent files must ' +
'carry the ${CLAUDE_PLUGIN_ROOT} form; see rewritePluginAgentMarkdown (issue #523).',
);
}
if (content.includes('embed-prompt.mjs') && !content.includes(AGENT_EMBED_FALLBACK)) {
throw new Error(
`Plugin rewrite drift: ${agentPath} carries an embed instruction without the sidecar ` +
"fallback sentence. The source sentence no longer matches the anchor in " +
'scripts/lib/plugin-paths.js (issue #523); update the fallback anchor to the new wording.',
);
}
}
/**
* Fail the build when the copied SKILL.md no longer matches the rewrite.
* The fallback-sentence replacement keys on the exact Setup step 1 text; if
* SKILL.src.md rewords it, replaceAll silently no-ops and the plugin ships
* the project path as its fallback. Loud beats wrong: the build stops here
* so plugin-paths.js gets updated alongside the source.
*/
export function verifyPluginSkillRewrite(skillMdPath) {
const content = fs.readFileSync(skillMdPath, 'utf-8');
if (!content.includes(SETUP_PLUGIN_TEXT)) {
throw new Error(
`Plugin rewrite drift: ${skillMdPath} is missing the <skill-base-dir> resolution sentence. ` +
"SKILL.src.md's Setup step 1 fallback sentence no longer matches the replacement in " +
'scripts/lib/plugin-paths.js (issue #523); update SETUP_FALLBACK_TEXT to the new wording.',
);
}
if (content.includes('Bash(node ')) {
throw new Error(
`Plugin rewrite drift: ${skillMdPath} still pre-approves a node script path. ` +
"SKILL.src.md's allowed-tools entry no longer matches the removal in " +
'scripts/lib/plugin-paths.js (issue #523); the plugin ships no node pre-approval.',
);
}
if (content.includes(CLAUDE_PROJECT_SCRIPTS_PATH)) {
throw new Error(
`Plugin rewrite drift: ${skillMdPath} still contains the project-relative scripts path ` +
`(${CLAUDE_PROJECT_SCRIPTS_PATH}). A wording or path shape in SKILL.src.md slipped past ` +
'the replacements in scripts/lib/plugin-paths.js (issue #523); the plugin copy must not ' +
"reference the project's scripts directory.",
);
}
}
/**
* Apply a rewrite to every .md file under dir, recursively. Defaults to
* the skill rewrite; the agents directory passes rewritePluginAgentMarkdown.
* Script files are left alone: the only project-relative paths in them
* (hook-admin.mjs) install project-scoped hooks via ${CLAUDE_PROJECT_DIR},
* which is that command's actual job.
*/
export function rewritePluginMarkdownTree(dir, rewrite = rewritePluginMarkdown) {
if (!fs.existsSync(dir)) return;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const entryPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
rewritePluginMarkdownTree(entryPath, rewrite);
} else if (entry.name.endsWith('.md')) {
const original = fs.readFileSync(entryPath, 'utf-8');
const rewritten = rewrite(original);
if (rewritten !== original) fs.writeFileSync(entryPath, rewritten);
}
}
}
+2
View File
@@ -46,6 +46,7 @@ export const SUITES = {
'tests/skills-cli.test.js', 'tests/skills-cli.test.js',
'tests/validate-plugin-versions.test.js', 'tests/validate-plugin-versions.test.js',
'tests/validate-plugin-manifest.test.js', 'tests/validate-plugin-manifest.test.js',
'tests/plugin-paths.test.js',
], ],
}, },
{ {
@@ -232,6 +233,7 @@ export const SUITES = {
/^skill\/agents\//, /^skill\/agents\//,
/^scripts\/build\.js$/, /^scripts\/build\.js$/,
/^scripts\/lib\/validate-plugin-manifest\.js$/, /^scripts\/lib\/validate-plugin-manifest\.js$/,
/^scripts\/lib\/plugin-paths\.js$/,
/^tests\/plugin-e2e\.test\.mjs$/, /^tests\/plugin-e2e\.test\.mjs$/,
], ],
commands: [ commands: [
+290
View File
@@ -0,0 +1,290 @@
/**
* Unit coverage for the plugin subtree script-path rewrite (issue #523).
*
* The ./plugin subtree copies the dist/claude-code output, whose
* {{scripts_path}} resolves to the project-relative
* `.claude/skills/impeccable/scripts`. Run from the plugin cache, that path
* points into the user's project: a plugin-only user gets MODULE_NOT_FOUND,
* and a dual-install user silently runs the project's older skill copy. The
* rewrite swaps every markdown instruction to the `<skill-base-dir>` form
* and drops the node pre-approval: no frontmatter rule can bind approval to
* the loaded plugin root, and an unbound wildcard would auto-approve any
* same-shaped path anywhere on disk.
*/
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import fs from 'fs';
import os from 'os';
import path from 'path';
import {
rewritePluginMarkdown,
rewritePluginAgentMarkdown,
rewritePluginMarkdownTree,
verifyPluginSkillRewrite,
verifyPluginAgentRewrite,
CLAUDE_PROJECT_SCRIPTS_PATH,
AGENT_EMBED_FALLBACK,
} from '../scripts/lib/plugin-paths.js';
describe('rewritePluginMarkdown', () => {
test('rewrites a script instruction to the quoted skill-base-dir form', () => {
const input = 'Run `node .claude/skills/impeccable/scripts/context.mjs` once per session.';
expect(rewritePluginMarkdown(input)).toBe(
'Run `node "<skill-base-dir>/scripts/context.mjs"` once per session.',
);
});
test('rewrites every occurrence, quoting the script path but not the arguments', () => {
const input = [
'node .claude/skills/impeccable/scripts/live.mjs',
'node .claude/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID done',
].join('\n');
const output = rewritePluginMarkdown(input);
expect(output).not.toContain(CLAUDE_PROJECT_SCRIPTS_PATH);
expect(output).toContain('node "<skill-base-dir>/scripts/live.mjs"');
expect(output).toContain('node "<skill-base-dir>/scripts/live-poll.mjs" --reply EVENT_ID done');
});
test('quotes commands already in the skill-base-dir form without double-quoting', () => {
// SKILL.src.md's Setup step 1 carries the token form natively; a base
// directory with spaces splits an unquoted path before node sees it.
const input =
'Run `node <skill-base-dir>/scripts/context.mjs` once per session. ' +
'Already quoted: `node "<skill-base-dir>/scripts/detect.mjs"`.';
expect(rewritePluginMarkdown(input)).toBe(
'Run `node "<skill-base-dir>/scripts/context.mjs"` once per session. ' +
'Already quoted: `node "<skill-base-dir>/scripts/detect.mjs"`.',
);
});
test('removes the node pre-approval instead of widening it', () => {
const frontmatter = [
'allowed-tools:',
' - Bash(npx impeccable *)',
' - Bash(node .claude/skills/impeccable/scripts/*)',
'---',
'',
].join('\n');
const output = rewritePluginMarkdown(frontmatter);
// The generic path rewrite alone would leave Bash(node <skill-base-dir>/scripts/*),
// a dead literal, and any wildcard replacement would auto-approve
// same-shaped paths outside the plugin. The line must go entirely.
expect(output).not.toContain('Bash(node ');
expect(output).toContain(' - Bash(npx impeccable *)\n---');
});
test('drops the project-path fallback clause from Setup step 1', () => {
const input =
'1. Run `node <skill-base-dir>/scripts/context.mjs` once per session, where `<skill-base-dir>` is the ' +
"loaded base directory the runtime reports for this skill; keep cwd at the user's project. " +
'That base directory resolves every `node .claude/skills/impeccable/scripts/...` command in this skill ' +
'and its references, and `.claude/skills/impeccable/scripts` is the fallback only when the runtime ' +
'reports no base directory. Pass a named source file or route as `--target <path>`.';
const output = rewritePluginMarkdown(input);
expect(output).toContain(
'Every `node "<skill-base-dir>/scripts/..."` command in this skill and its references resolves against that base directory.',
);
// The naive rewrite would keep the fallback clause and name the token as
// its own fallback for when there is no base directory to resolve it.
expect(output).not.toContain('fallback');
expect(output).not.toContain(CLAUDE_PROJECT_SCRIPTS_PATH);
});
test('leaves unrelated project-relative paths alone', () => {
const input = 'State lives in `.impeccable/live/roots.json` and `.claude/settings.json`.';
expect(rewritePluginMarkdown(input)).toBe(input);
});
});
describe('rewritePluginAgentMarkdown', () => {
// The real source sentence shape: command, purpose clause, next sentence.
const sourceStep =
'after every generation, run `node .claude/skills/impeccable/scripts/embed-prompt.mjs <asset> ' +
'--prompt "<the prompt used>"` so the prompt lives inside the image itself. The build thread ' +
'composes what you made.';
test('rewrites agent instructions to the quoted plugin-root variable form', () => {
// A spawned agent never loads SKILL.md, so the <skill-base-dir> token
// Setup defines is unresolvable in its prompt. Claude Code substitutes
// ${CLAUDE_PLUGIN_ROOT} inline in plugin agent content.
const output = rewritePluginAgentMarkdown(sourceStep);
expect(output).toContain(
'run `node "${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/embed-prompt.mjs" <asset> --prompt "<the prompt used>"`',
);
});
test('appends the sidecar fallback as its own sentence, not mid-sentence', () => {
const output = rewritePluginAgentMarkdown(sourceStep);
// The fallback follows the full embed sentence and precedes the next one.
expect(output).toContain(
`so the prompt lives inside the image itself.${AGENT_EMBED_FALLBACK} The build thread`,
);
});
test('never emits the skill-base-dir token into an agent file', () => {
const output = rewritePluginAgentMarkdown(sourceStep);
expect(output).not.toContain('<skill-base-dir>');
expect(output).not.toContain(CLAUDE_PROJECT_SCRIPTS_PATH);
});
});
describe('verifyPluginAgentRewrite', () => {
let root;
beforeEach(() => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-agent-verify-'));
});
afterEach(() => {
fs.rmSync(root, { recursive: true, force: true });
});
const writeAgent = (contents) => {
const p = path.join(root, 'agent.md');
fs.writeFileSync(p, contents);
return p;
};
const sourceStep =
'run `node .claude/skills/impeccable/scripts/embed-prompt.mjs <asset> --prompt "<p>"` ' +
'so the prompt lives inside the image itself. Next sentence.';
test('accepts a correctly rewritten agent file', () => {
const p = writeAgent(rewritePluginAgentMarkdown(sourceStep));
expect(() => verifyPluginAgentRewrite(p)).not.toThrow();
});
test('fails when an unresolvable path form survives', () => {
const p = writeAgent('run `node "<skill-base-dir>/scripts/embed-prompt.mjs"` please.');
expect(() => verifyPluginAgentRewrite(p)).toThrow(/cannot\s+resolve/);
});
test('fails when the embed instruction lost its fallback sentence', () => {
// Simulate a source rewording that breaks the fallback anchor: the
// sentence-splice regex no-ops when no period follows the command.
const reworded = sourceStep.replace(
' so the prompt lives inside the image itself. Next sentence.',
' -- no closing period',
);
const p = writeAgent(rewritePluginAgentMarkdown(reworded));
expect(() => verifyPluginAgentRewrite(p)).toThrow(/sidecar/);
});
});
describe('rewritePluginMarkdownTree', () => {
let root;
beforeEach(() => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-plugin-paths-'));
});
afterEach(() => {
fs.rmSync(root, { recursive: true, force: true });
});
test('rewrites .md files recursively and leaves scripts untouched', () => {
const write = (rel, contents) => {
const abs = path.join(root, rel);
fs.mkdirSync(path.dirname(abs), { recursive: true });
fs.writeFileSync(abs, contents);
};
write('SKILL.md', 'Run `node .claude/skills/impeccable/scripts/context.mjs`.');
write('reference/live.md', 'node .claude/skills/impeccable/scripts/live.mjs');
// hook-admin.mjs installs project-scoped hooks; its project path is correct.
write(
'scripts/hook-admin.mjs',
'const cmd = \'node "${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/hook.mjs"\';',
);
rewritePluginMarkdownTree(root);
expect(fs.readFileSync(path.join(root, 'SKILL.md'), 'utf-8')).toBe(
'Run `node "<skill-base-dir>/scripts/context.mjs"`.',
);
expect(fs.readFileSync(path.join(root, 'reference/live.md'), 'utf-8')).toBe(
'node "<skill-base-dir>/scripts/live.mjs"',
);
expect(fs.readFileSync(path.join(root, 'scripts/hook-admin.mjs'), 'utf-8')).toContain(
'${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/hook.mjs',
);
});
test('applies the agent rewrite when passed for an agents tree', () => {
const agentsDir = path.join(root, 'agents');
fs.mkdirSync(agentsDir, { recursive: true });
fs.writeFileSync(
path.join(agentsDir, 'impeccable-asset-producer.md'),
'run `node .claude/skills/impeccable/scripts/embed-prompt.mjs <asset>`',
);
rewritePluginMarkdownTree(agentsDir, rewritePluginAgentMarkdown);
expect(fs.readFileSync(path.join(agentsDir, 'impeccable-asset-producer.md'), 'utf-8')).toBe(
'run `node "${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/embed-prompt.mjs" <asset>`',
);
});
test('is a no-op on a missing directory', () => {
expect(() => rewritePluginMarkdownTree(path.join(root, 'does-not-exist'))).not.toThrow();
});
});
describe('verifyPluginSkillRewrite', () => {
let root;
beforeEach(() => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-plugin-verify-'));
});
afterEach(() => {
fs.rmSync(root, { recursive: true, force: true });
});
const writeSkill = (contents) => {
const p = path.join(root, 'SKILL.md');
fs.writeFileSync(p, contents);
return p;
};
const goodSkill = [
'allowed-tools:',
' - Bash(node .claude/skills/impeccable/scripts/*)',
'',
'1. Run `node <skill-base-dir>/scripts/context.mjs` once per session, where `<skill-base-dir>` is the ' +
"loaded base directory the runtime reports for this skill; keep cwd at the user's project. " +
'That base directory resolves every `node .claude/skills/impeccable/scripts/...` command in this skill ' +
'and its references, and `.claude/skills/impeccable/scripts` is the fallback only when the runtime ' +
'reports no base directory.',
].join('\n');
test('accepts a correctly rewritten SKILL.md', () => {
const p = writeSkill(rewritePluginMarkdown(goodSkill));
expect(() => verifyPluginSkillRewrite(p)).not.toThrow();
});
test('fails the build when the Setup fallback sentence no longer matched', () => {
// Simulate SKILL.src.md rewording step 1: the sentence replacement
// no-ops, so the plugin copy keeps the project path as its fallback.
const reworded = goodSkill.replace('is the fallback only when', 'is used only when');
const p = writeSkill(rewritePluginMarkdown(reworded));
expect(() => verifyPluginSkillRewrite(p)).toThrow(/Setup step 1 fallback sentence/);
});
test('fails the build when a node pre-approval survives the removal', () => {
const reworded = goodSkill.replace(
'Bash(node .claude/skills/impeccable/scripts/*)',
'Bash(node .claude/skills/impeccable/scripts/**)',
);
const p = writeSkill(rewritePluginMarkdown(reworded));
expect(() => verifyPluginSkillRewrite(p)).toThrow(/pre-approves a node script path/);
});
test('fails the build when the project-relative scripts path survives at all', () => {
// Simulate a path shape the replacements don't know: the rewritten copy
// still names the project scripts directory somewhere new.
const p = writeSkill(
rewritePluginMarkdown(goodSkill) +
'\nState lives next to `.claude/skills/impeccable/scripts` on disk.',
);
expect(() => verifyPluginSkillRewrite(p)).toThrow(/still contains the project-relative scripts path/);
});
});