mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23b6b9cc0e | ||
|
|
735a0f4e4f | ||
|
|
8cef296996 | ||
|
|
afc974d630 | ||
|
|
93a13f9882 | ||
|
|
c32daaf3b0 | ||
|
|
e7e923c4ef |
@@ -0,0 +1,92 @@
|
||||
# Codex: Visual Direction & Asset Production
|
||||
|
||||
This file is loaded by `$impeccable craft` when the harness has native image generation (currently Codex via `image_gen`). Other harnesses skip it. It covers the two craft steps that depend on real image generation: landing the visual direction, and producing the raster assets the implementation will compose.
|
||||
|
||||
Read this *before* generating any images. The order matters, and the per-step user pauses are what keep generated imagery from drifting away from the brief.
|
||||
|
||||
## Step A: Explore Directions with the User
|
||||
|
||||
Before generating anything, run a brief direction conversation grounded in the shape brief.
|
||||
|
||||
Ask **2-3 targeted questions** about visual lane, color strategy, atmosphere, and named anchor references. Don't enumerate generic menus; tie each question to the shape brief's answers. Example shape-grounded questions:
|
||||
|
||||
- "Brief says 'editorial restraint, Klim-adjacent.' Are we closer to a quiet specimen page or a magazine-spread feel with hero imagery?"
|
||||
- "Palette strategy from shape was 'Committed.' Want it warm-grounded (deep oxblood + cream) or cool-grounded (slate + paper white)?"
|
||||
|
||||
Stop and wait for answers. These pin the palette before any pixel gets generated.
|
||||
|
||||
## Step B: Generate the Brand Palette First
|
||||
|
||||
Generate **one** palette artifact before any mocks. This is a small, focused image: typography pairing on the chosen background, primary + accent color swatches, one signature ornament or motif. Single image, single pass.
|
||||
|
||||
Why palette first: mocks generated against a vague color sense produce noise that drowns out the structural decisions. A confirmed palette is the first concrete contract for everything downstream.
|
||||
|
||||
Show the palette to the user. Ask one question: "This is the palette I'm locking in for the mocks. Confirm, or call out what to shift?"
|
||||
|
||||
Wait for confirmation. Do not generate mocks against an unconfirmed palette.
|
||||
|
||||
## Step C: Generate 1-3 Visual Mocks Against the Palette
|
||||
|
||||
Once the palette is confirmed, generate **1 to 3** high-fidelity north-star comps. Each mock must use the confirmed palette and typography. Mocks differ in *structural* direction (hierarchy, topology, density, composition), not in color or motif.
|
||||
|
||||
- Brand work: push visual identity, composition, mood, and signature motifs.
|
||||
- Product work: push hierarchy, topology, density, tone, grounded in realistic product structure.
|
||||
- Landing pages and long-form brand surfaces: show enough of the second fold to establish the system beyond the hero.
|
||||
|
||||
Use the `image_gen` tool directly (or via the imagegen skill when available). Don't ask the user to install anything.
|
||||
|
||||
## Step D: Approval Loop
|
||||
|
||||
Show the comps. Ask what carries forward. Iterate until **one direction is approved** or the user explicitly delegates.
|
||||
|
||||
If the user delegates, pick the strongest direction and explain it from the brief, not personal taste.
|
||||
|
||||
Before moving to assets, summarize what to carry into code and what *not* to literalize from the mock. This is the handoff between visual exploration and semantic implementation.
|
||||
|
||||
## Step E: Mock Fidelity Inventory
|
||||
|
||||
Inventory the approved mock's major visible ingredients. For each, decide implementation: semantic HTML/CSS/SVG, generated raster, sourced raster, icon library, canvas/WebGL, or accepted omission.
|
||||
|
||||
Common ingredients to inventory:
|
||||
|
||||
- Hero silhouette and dominant composition
|
||||
- Signature motifs (planets, devices, portraits, charts, route lines, insets, badges, etc.)
|
||||
- Nav and primary CTA treatment
|
||||
- Section sequence, especially the second fold
|
||||
- Image-native content the concept depends on
|
||||
- Typography, density, color/material treatment, motion cues
|
||||
|
||||
Treat the mock as a north star, not a screenshot to trace. Don't rasterize core UI text. But if the live result lacks the mock's major ingredients, the implementation is wrong.
|
||||
|
||||
If a photographic, architectural, product, or place-led mock becomes generic CSS scenery, decorative diagrams, bullets, or copy, stop and fix it. That's a broken implementation, not a harmless interpretation.
|
||||
|
||||
Don't substitute a different hero composition or visual driver post-approval without user sign-off.
|
||||
|
||||
## Step F: Asset Slicing via the Asset Producer
|
||||
|
||||
Raster ingredients identified in Step E need clean production assets. Use the bundled `impeccable_asset_producer` subagent rather than producing inline.
|
||||
|
||||
Spawn it as a scoped subagent. If you do not have explicit permission to use agents, stop and ask:
|
||||
|
||||
```text
|
||||
Asset production will work better as a scoped subagent job. Should I spawn the Impeccable asset producer subagent for this step?
|
||||
```
|
||||
|
||||
Pass to the agent:
|
||||
|
||||
- Approved mock path or screenshot reference
|
||||
- Crop paths or a contact sheet with crop ids
|
||||
- Output directory
|
||||
- Required dimensions, format, transparency needs
|
||||
- Avoid list
|
||||
- Notes on what should remain semantic HTML/CSS/SVG instead of raster
|
||||
|
||||
Attach image generation capability to the spawned agent when the harness supports it. Do **not** load image-generation reference material into the parent thread.
|
||||
|
||||
Inline asset production is allowed only if the user declines subagents, the harness cannot spawn the authorized agent, or the user explicitly asks for single-thread mode.
|
||||
|
||||
Prefer HTML/CSS/SVG/canvas when they can credibly reproduce an ingredient; reach for real, generated, or stock imagery when the mock or subject matter calls for actual visual content.
|
||||
|
||||
## After This File
|
||||
|
||||
Once Steps A through F are complete, return to `craft.md` Step 5 (Build to Production Quality). The implementation builds against the confirmed palette, approved mock, and the assets the producer wrote.
|
||||
@@ -51,64 +51,15 @@ Then add references based on the brief's needs:
|
||||
- Responsive requirements? Consult [responsive-design.md](responsive-design.md)
|
||||
- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.md)
|
||||
|
||||
## Step 3: Land the Visual Direction (Capability-Gated)
|
||||
## Step 3: Visual Direction & Assets (Harness-Gated)
|
||||
|
||||
Generate high-fidelity visual comps before implementation when all three are true:
|
||||
If the harness has **native image generation** (currently Codex via `image_gen`), this step is mandatory. **Stop and load [codex.md](codex.md)**. It covers palette generation, mock exploration, the approval loop, mock-fidelity inventory, and asset slicing via the `impeccable_asset_producer` subagent. Follow Steps A-F in that file, then return here for Step 4.
|
||||
|
||||
- The work is net-new or visually open-ended enough that composition exploration will improve the build.
|
||||
- The brief's scope is mid-fi, high-fi, or production-ready.
|
||||
- The harness gives you native image generation (Codex's `image_gen`, an equivalent MCP tool). Don't ask the user to set up external APIs.
|
||||
If the harness lacks native image generation, **state in one line that the visual-direction-by-generation step is being skipped because the harness lacks native image generation, then proceed**. The one-line announcement is required; it forces a conscious decision instead of letting the step quietly evaporate. The brief is your only visual reference. Implement directly from it, treating any named anchor references and the brief's "Design Direction" as the contract.
|
||||
|
||||
When met, this step is mandatory for both brand and product work. If image generation isn't available, skip silently. "The eventual UI is semantic/code-native/accessible" is not a reason to skip; those are implementation requirements, not exploration ones.
|
||||
Whether you generated mocks or not: don't replace required imagery with generic cards, bullets, emoji, fake metrics, decorative CSS panels, or filler copy. Image-led briefs (restaurants, hotels, magazines, photography, hobbyist communities, food, travel, fashion, product) need real or sourced imagery in the build, not CSS scenery.
|
||||
|
||||
### What to generate
|
||||
|
||||
Generate **1 to 3** high-fidelity north-star comps from the confirmed brief. If shape already produced direction probes, resolve the winning lane further, not an unrelated exploration. Comps must differ in primary visual direction, not just color.
|
||||
|
||||
- Brand work: push visual identity, composition, and mood aggressively.
|
||||
- Product work: push hierarchy, topology, density, tone, grounded in realistic product structure.
|
||||
- Landing pages and long-form brand surfaces: show enough of the second fold to establish the system beyond the hero.
|
||||
|
||||
### Approval loop
|
||||
|
||||
Show the comps and ask what carries forward. Iterate until one direction is approved or the user delegates. If the user delegates, pick the strongest direction and explain it from the brief, not personal taste.
|
||||
|
||||
Before Step 4, summarize what to carry into code and what **not** to literalize from the mock. This is the handoff between visual exploration and semantic implementation.
|
||||
|
||||
### Mock fidelity inventory
|
||||
|
||||
Inventory the approved mock's major visible ingredients:
|
||||
|
||||
- Hero silhouette and dominant composition
|
||||
- Signature motifs (planets, devices, portraits, charts, route lines, insets, badges, etc.)
|
||||
- Nav and primary CTA treatment
|
||||
- Section sequence, especially the second fold
|
||||
- Image-native content the concept depends on
|
||||
- Typography, density, color/material treatment, motion cues
|
||||
|
||||
For each, decide implementation: semantic HTML/CSS/SVG, generated asset, sourced asset, icon library, canvas/WebGL, or accepted omission. Don't substitute a different hero composition or visual driver post-approval without user sign-off.
|
||||
|
||||
If a photographic, architectural, product, or place-led mock becomes generic CSS scenery / decorative diagrams / bullets / copy, stop and fix it. That's a broken implementation, not a harmless interpretation.
|
||||
|
||||
Treat the mock as a north star, not a screenshot to trace. Don't rasterize core UI text. But if the live result lacks the mock's major ingredients, the implementation is wrong.
|
||||
|
||||
## Step 4: Asset Extraction (Need-Gated)
|
||||
|
||||
If the approved direction needs raster assets, create them before building. Do not replace required imagery with generic cards, bullets, emoji, fake metrics, decorative CSS panels, or filler copy.
|
||||
|
||||
Use the native asset producer (`impeccable_asset_producer` in Codex, `impeccable-asset-producer` in Claude Code) to create clean assets from the hi-fi mock and crops. If you do not have explicit permission to use agents, stop and ask:
|
||||
|
||||
```text
|
||||
Asset production will work better as a scoped subagent job. Should I spawn the Impeccable asset producer subagent for this step?
|
||||
```
|
||||
|
||||
Do not skip asset production or silently do it inline. Inline asset production is allowed only if the user declines subagents, the harness cannot spawn the authorized agent, or the user explicitly asks for single-thread mode.
|
||||
|
||||
Pass the approved mock, crop/contact-sheet paths, output directory, dimensions/formats, transparency needs, constraints, and avoid list to the asset producer. Attach image generation capability to the spawned agent when the harness supports it; do not load image-generation reference material into the parent thread first.
|
||||
|
||||
Prefer HTML/CSS/SVG/canvas when they can credibly reproduce an ingredient; use real/generated/stock imagery when the mock or subject matter calls for actual visual content.
|
||||
|
||||
## Step 5: Build to Production Quality
|
||||
## Step 4: Build to Production Quality
|
||||
|
||||
Implement the feature following the design brief. Build in passes so structure, visual system, states, motion/media, and responsive behavior each get deliberate attention. The list below is the definition of done, not inspiration.
|
||||
|
||||
@@ -130,7 +81,7 @@ Implement the feature following the design brief. Build in passes so structure,
|
||||
- **Technically clean.** Production build passes, no console errors, no avoidable layout shift, no needless dependencies, no broken asset paths.
|
||||
- **Ask when uncertain.** If a discovery materially changes the brief or approved direction, stop and ask. Don't guess.
|
||||
|
||||
## Step 6: Iterate Visually
|
||||
## Step 5: Iterate Visually
|
||||
|
||||
Look at what you built like a designer would. Your eyes are whatever the harness gives you: a connected browser, a screenshotting tool, Playwright, or asking the user. Use them for responsive testing (mobile, tablet, desktop minimum) and general visual validation.
|
||||
|
||||
@@ -144,7 +95,7 @@ Actively check: responsive behavior (composes, not shrinks), every state (empty
|
||||
|
||||
Detector or QA output is defect evidence only; never proof the work is finished.
|
||||
|
||||
## Step 7: Present
|
||||
## Step 6: Present
|
||||
|
||||
Present the result to the user:
|
||||
- Show the feature in its primary state
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
> **Additional context needed**: what the interface is trying to accomplish.
|
||||
|
||||
### Setup: Resolve Target and Load Ignore List
|
||||
|
||||
Before gathering assessments, do two small bookkeeping steps. They cost almost nothing and they're what makes critique iterative across runs.
|
||||
|
||||
1. **Resolve the primary artifact.** The user's phrasing ("the homepage", "the pricing flow") is not stable enough to track across runs. Resolve it to a concrete file path or URL: the same one you'd already need to scan code or open in a browser. Examples:
|
||||
- "the homepage" → `site/pages/index.astro` (or `http://localhost:3000/` if you're inspecting live)
|
||||
- "the settings modal" → the primary component file (e.g., `src/components/Settings.tsx`)
|
||||
- "this page" → the URL or the page's source file
|
||||
Prefer the source file path over the dev-server URL when both exist; ports drift between runs (`bun dev` vs `bun preview`), file paths don't.
|
||||
|
||||
2. **Compute the slug.** Run:
|
||||
```bash
|
||||
node .agents/skills/impeccable/scripts/critique-storage.mjs slug "<resolved-path-or-url>"
|
||||
```
|
||||
Keep the printed slug. It identifies this target's stream across runs. If the command exits non-zero ("no stable slug for input"), skip persistence for this run and tell the user; the trend won't update but the critique still goes ahead.
|
||||
|
||||
3. **Read the ignore list** at `.impeccable/critique/ignore.md` if it exists. Plain markdown; each non-empty, non-comment line is something the user has marked as "do not re-raise" (deferred tradeoffs, designer-intended deviations, detector false-positives the user accepts). When a finding's text matches a line here (case-insensitive substring against rule name or snippet), **drop it silently**. Do not mention it in the report. This is the ONLY input critique consumes from prior runs; anchoring on prior findings would defeat the point of independent assessment.
|
||||
|
||||
### Gather Assessments
|
||||
|
||||
Launch two independent assessments. **Neither may see the other's output.** This isolation is what makes the combined score honest. Running both in one head silently anchors them to each other; do not shortcut it for cost, speed, or context-size reasons.
|
||||
@@ -164,6 +182,36 @@ Provocative questions that might unlock better solutions:
|
||||
- Prioritize ruthlessly. If everything is important, nothing is.
|
||||
- Don't soften criticism. Developers need honest feedback to ship great design.
|
||||
|
||||
### Persist the Snapshot
|
||||
|
||||
Once the report above is finalized, write it to `.impeccable/critique/` so the user can refer back, and so `$impeccable polish` can pick up the priority issues without a copy-paste.
|
||||
|
||||
Skip this step if the Setup slug was null (vague or root-level target).
|
||||
|
||||
1. **Write the body to a temp file** so you can pipe it to the helper. Use the full report (heuristic table, anti-patterns verdict, priority issues, persona red flags) but stop before the "Ask the User" / "Recommended Actions" sections that come later.
|
||||
|
||||
2. **Pass the structured metadata** through `IMPECCABLE_CRITIQUE_META` (JSON), then run the write command:
|
||||
```bash
|
||||
IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"p0_count":<n>,"p1_count":<n>}' \
|
||||
node .agents/skills/impeccable/scripts/critique-storage.mjs write <slug> <body-file>
|
||||
```
|
||||
The helper prints the absolute path it wrote.
|
||||
|
||||
3. **Read the trend** for context:
|
||||
```bash
|
||||
node .agents/skills/impeccable/scripts/critique-storage.mjs trend <slug> 5
|
||||
```
|
||||
This returns a JSON array of the last 5 frontmatter entries (including the one you just wrote).
|
||||
|
||||
4. **Append a single line to the user-visible output**, after the report and before the questions:
|
||||
|
||||
> **Trend for `<slug>` (last 5 runs): 24 → 28 → 32 → 29 → 32**
|
||||
> Wrote `.impeccable/critique/<filename>`.
|
||||
|
||||
If this is the first run for the slug, the trend is just one score; say so: "First run for this target, no trend yet."
|
||||
|
||||
This is fire-and-forget. Do not show the user the helper's JSON output; only the human-readable trend line and the written path. Failures here should not block the rest of the flow; print the error and move on.
|
||||
|
||||
### Ask the User
|
||||
|
||||
**After presenting findings**, use targeted questions based on what was actually found. STOP and use Codex's structured user-input/question tool when available; if unavailable, ask directly in chat to clarify what you cannot infer. These answers will shape the action plan.
|
||||
|
||||
@@ -35,7 +35,14 @@ Understand the current state and goals before touching anything:
|
||||
- Loading and transition smoothness
|
||||
- Information architecture and flow drift (does this feature reveal complexity the way neighboring features do?)
|
||||
|
||||
4. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent; never perfect one corner while leaving another rough.
|
||||
4. **Pull in any prior critique** (optional signal): If `$impeccable critique` has been run on the same target, its priority issues are a useful prior for what to address first. Resolve the target to a file path or URL, then:
|
||||
```bash
|
||||
slug=$(node .agents/skills/impeccable/scripts/critique-storage.mjs slug "<resolved>")
|
||||
node .agents/skills/impeccable/scripts/critique-storage.mjs latest "$slug"
|
||||
```
|
||||
Exit 0 with body = found; fold the P0/P1 items into your polish list and mention the snapshot path so the user sees what you read. Exit 2 = no snapshot, continue without it. The critique is one input among many. Do your own pass either way.
|
||||
|
||||
5. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent; never perfect one corner while leaving another rough.
|
||||
|
||||
**CRITICAL**: Polish is the last step, not the first. Don't polish work that's not functionally complete.
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ After the discovery interview, generate a small set of visual direction probes *
|
||||
- The requested fidelity is **mid-fi, high-fi, or production-ready**. Skip for sketch-only planning.
|
||||
- The current harness gives you native image generation (Codex's `image_gen`, an equivalent MCP tool, or similar). Don't ask the user to install APIs or tooling.
|
||||
|
||||
When those conditions are met, this step is mandatory. If image generation isn't natively available, skip silently and proceed; don't announce the skip.
|
||||
When those conditions are met, this step is mandatory. If image generation isn't natively available, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed. The one-line announcement is required, not optional; it forces a conscious decision instead of letting the step quietly evaporate.
|
||||
|
||||
Use probes to explore visual lanes, not to replace the brief.
|
||||
|
||||
@@ -107,7 +107,7 @@ The probes should differ in primary visual direction (hierarchy, topology, densi
|
||||
- Do **not** treat generated imagery as final UX specification, final copy, or final accessibility behavior.
|
||||
- Do **not** use this step for minor refinements of existing work. It's for shaping a new surface or clarifying a big directional choice.
|
||||
|
||||
If image generation isn't natively available, skip this phase silently and proceed.
|
||||
If image generation isn't natively available, announce the skip in one line and proceed to the design brief.
|
||||
|
||||
## Phase 2: Design Brief
|
||||
|
||||
@@ -120,7 +120,7 @@ After the interview and any required probes, present a brief and **end your resp
|
||||
|
||||
Don't pad a clear brief into a long one to look thorough. A 70-line brief restating answers the user just gave is noise, not rigor. Equally, don't skip the confirmation pause to look efficient: the pause is the point.
|
||||
|
||||
If the user already said "approved" or "go" during discovery for the *exact direction you'd present*, that counts as confirmation; you may proceed without asking again. But if your brief adds anything the user hasn't seen and approved, you must stop and confirm.
|
||||
Present the brief, then **stop and wait for explicit confirmation**. You are not the judge of whether the user already approved. Even when the brief feels obviously right, ask once and wait. The pause is what separates shape from premature implementation.
|
||||
|
||||
### Brief Structure
|
||||
|
||||
@@ -158,8 +158,8 @@ Anything genuinely unresolved. Don't list "open questions" you've already recomm
|
||||
|
||||
---
|
||||
|
||||
STOP and use Codex's structured user-input/question tool when available; if unavailable, ask directly in chat to clarify what you cannot infer.
|
||||
STOP and use Codex's structured user-input/question tool when available; if unavailable, ask directly in chat to clarify what you cannot infer. Ask for explicit confirmation of the brief before finishing.
|
||||
|
||||
If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the user confirms direction (or the user already gave a clear go during discovery, which counts).
|
||||
If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the user confirms direction.
|
||||
|
||||
Once confirmed, the brief is complete. The user can now hand it to $impeccable, or use it to guide any other implementation approach. (If the user wants the full discovery-then-build flow in one step, they should use $impeccable craft instead, which runs this command internally.)
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Critique persistence helper.
|
||||
*
|
||||
* Each run of /impeccable critique writes a per-target snapshot to
|
||||
* .impeccable/critique/<timestamp>__<slug>.md
|
||||
* with a small YAML frontmatter carrying the score + P0/P1 counts.
|
||||
*
|
||||
* /impeccable polish reads the latest matching snapshot at start as its
|
||||
* fix backlog. No other skill auto-reads critique output.
|
||||
*
|
||||
* The slug is derived mechanically from the *resolved* primary artifact
|
||||
* (file path or URL), never from the user's natural-language phrasing.
|
||||
* Slug stability across runs is what lets the trend display work.
|
||||
*
|
||||
* CLI entry points (called from skill instructions):
|
||||
* node critique-storage.mjs slug <resolved-target>
|
||||
* node critique-storage.mjs write <slug> <snapshot-body-file>
|
||||
* node critique-storage.mjs latest <slug>
|
||||
* node critique-storage.mjs trend <slug> [limit]
|
||||
*
|
||||
* Note: there is intentionally no `ignore` subcommand. ignore.md is a plain
|
||||
* markdown file; the model reads it directly with its file-read tool. This
|
||||
* helper only exists for operations the model can't trivially do inline
|
||||
* (normalizing paths, generating filenames, globbing + parsing frontmatter).
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { getCritiqueDir } from './impeccable-paths.mjs';
|
||||
|
||||
const SLUG_MAX = 50;
|
||||
|
||||
/**
|
||||
* Mechanically derive a slug from a resolved target. Returns null if the
|
||||
* input doesn't look like a stable identifier (empty, project root, etc).
|
||||
*
|
||||
* Accepts file paths and URLs. The model resolves "the homepage" to a
|
||||
* concrete artifact before calling this — we never slug a natural-language
|
||||
* phrase.
|
||||
*/
|
||||
export function slugFromTarget(resolved, { cwd = process.cwd() } = {}) {
|
||||
if (!resolved || typeof resolved !== 'string') return null;
|
||||
const trimmed = resolved.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
// URL
|
||||
if (/^https?:\/\//i.test(trimmed)) {
|
||||
let url;
|
||||
try { url = new URL(trimmed); } catch { return null; }
|
||||
const hostPath = `${url.hostname}${url.pathname}`;
|
||||
return kebab(hostPath);
|
||||
}
|
||||
|
||||
// File path. Make it project-relative so two devs critiquing the same
|
||||
// checkout get the same slug regardless of where their repo is cloned.
|
||||
const abs = path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
|
||||
let rel = path.relative(cwd, abs);
|
||||
// If the target is outside cwd, fall back to the basename so we still
|
||||
// produce a stable slug (vs the absolute path, which would include
|
||||
// home dirs / usernames).
|
||||
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
||||
rel = path.basename(abs);
|
||||
}
|
||||
if (!rel || rel === '.' || rel === '') return null;
|
||||
return kebab(rel);
|
||||
}
|
||||
|
||||
function kebab(s) {
|
||||
const slug = s
|
||||
.toLowerCase()
|
||||
.replace(/[/\\.]+/g, '-')
|
||||
.replace(/[^a-z0-9-]+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
if (!slug) return null;
|
||||
// Cap from the tail — the tail (filename) is more identifying than the
|
||||
// top-level directory.
|
||||
return slug.length <= SLUG_MAX ? slug : slug.slice(slug.length - SLUG_MAX).replace(/^-/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filename-safe UTC ISO timestamp: hyphens for separators, trailing Z.
|
||||
* Plain colons aren't allowed on Windows filesystems.
|
||||
*/
|
||||
export function nowFilenameStamp(date = new Date()) {
|
||||
const iso = date.toISOString(); // 2026-05-12T18:30:00.123Z
|
||||
return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z');
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a snapshot for `slug`. `meta` carries the small structured frontmatter
|
||||
* keys read back by readTrend(). `body` is the human-readable critique
|
||||
* report (everything below the frontmatter).
|
||||
*
|
||||
* Returns the absolute path written.
|
||||
*/
|
||||
export function writeSnapshot({ slug, meta, body, cwd = process.cwd(), now = new Date() }) {
|
||||
if (!slug) throw new Error('writeSnapshot requires a slug');
|
||||
const dir = getCritiqueDir(cwd);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const timestamp = nowFilenameStamp(now);
|
||||
const filePath = path.join(dir, `${timestamp}__${slug}.md`);
|
||||
// Spread `meta` first so internally computed `timestamp` and `slug`
|
||||
// always win. Otherwise a caller-supplied meta blob (parsed from the
|
||||
// IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the
|
||||
// filename in disagreement with its frontmatter and corrupting trends.
|
||||
const front = serializeFrontmatter({ ...meta, timestamp, slug });
|
||||
fs.writeFileSync(filePath, `${front}\n${body.trim()}\n`, 'utf-8');
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function serializeFrontmatter(obj) {
|
||||
const lines = ['---'];
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
const str = typeof value === 'string' ? value : String(value);
|
||||
// Quote strings that contain : or # to keep parsing simple.
|
||||
const needsQuotes = typeof value === 'string' && /[:#]/.test(str);
|
||||
lines.push(`${key}: ${needsQuotes ? JSON.stringify(str) : str}`);
|
||||
}
|
||||
lines.push('---');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function parseFrontmatter(text) {
|
||||
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
if (!match) return {};
|
||||
const out = {};
|
||||
for (const line of match[1].split(/\r?\n/)) {
|
||||
const colon = line.indexOf(':');
|
||||
if (colon < 0) continue;
|
||||
const key = line.slice(0, colon).trim();
|
||||
let value = line.slice(colon + 1).trim();
|
||||
if (/^".*"$/.test(value)) {
|
||||
try { value = JSON.parse(value); } catch { /* leave as-is */ }
|
||||
} else if (/^-?\d+$/.test(value)) {
|
||||
value = Number(value);
|
||||
}
|
||||
out[key] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all snapshot files for `slug`, sorted oldest → newest.
|
||||
*/
|
||||
function listSnapshotsForSlug(slug, cwd) {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const suffix = `__${slug}.md`;
|
||||
return fs.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(suffix))
|
||||
.sort()
|
||||
.map((f) => path.join(dir, f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent snapshot for `slug`, or null. Polish reads this
|
||||
* to find its fix backlog when the slug matches.
|
||||
*/
|
||||
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
if (!all.length) return null;
|
||||
const latest = all[all.length - 1];
|
||||
const body = fs.readFileSync(latest, 'utf-8');
|
||||
return { path: latest, body, meta: parseFrontmatter(body) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last `limit` snapshots' frontmatter, oldest → newest.
|
||||
* Critique appends a one-line trend to its output using this.
|
||||
*/
|
||||
export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
const slice = all.slice(-limit);
|
||||
return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8')));
|
||||
}
|
||||
|
||||
// ---- CLI ---------------------------------------------------------------
|
||||
|
||||
function main(argv) {
|
||||
const [cmd, ...args] = argv;
|
||||
switch (cmd) {
|
||||
case 'slug': {
|
||||
const slug = slugFromTarget(args[0]);
|
||||
if (!slug) { process.stderr.write('no stable slug for input\n'); process.exit(1); }
|
||||
process.stdout.write(`${slug}\n`);
|
||||
return;
|
||||
}
|
||||
case 'write': {
|
||||
const [slug, bodyFile] = args;
|
||||
if (!slug || !bodyFile) { process.stderr.write('usage: write <slug> <body-file>\n'); process.exit(1); }
|
||||
const raw = fs.readFileSync(bodyFile, 'utf-8');
|
||||
// The body file may be a full report. The caller passes the meta as
|
||||
// a JSON object on stdin if it wants structured frontmatter; otherwise
|
||||
// we write with minimal metadata.
|
||||
let meta = {};
|
||||
const metaArg = process.env.IMPECCABLE_CRITIQUE_META;
|
||||
if (metaArg) {
|
||||
try { meta = JSON.parse(metaArg); } catch { /* ignore */ }
|
||||
}
|
||||
const out = writeSnapshot({ slug, meta, body: raw });
|
||||
process.stdout.write(`${out}\n`);
|
||||
return;
|
||||
}
|
||||
case 'latest': {
|
||||
const latest = readLatestSnapshot(args[0]);
|
||||
if (!latest) { process.exit(2); }
|
||||
process.stdout.write(latest.body);
|
||||
return;
|
||||
}
|
||||
case 'trend': {
|
||||
const rows = readTrend(args[0], { limit: args[1] ? Number(args[1]) : 5 });
|
||||
process.stdout.write(JSON.stringify(rows, null, 2) + '\n');
|
||||
return;
|
||||
}
|
||||
default:
|
||||
process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend> [args]\n');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main(process.argv.slice(2));
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import path from 'node:path';
|
||||
|
||||
export const IMPECCABLE_DIR = '.impeccable';
|
||||
export const LIVE_DIR = 'live';
|
||||
export const CRITIQUE_DIR = 'critique';
|
||||
|
||||
export function getImpeccableDir(cwd = process.cwd()) {
|
||||
return path.join(cwd, IMPECCABLE_DIR);
|
||||
@@ -96,6 +97,10 @@ export function getLiveAnnotationsDir(cwd = process.cwd()) {
|
||||
return path.join(getLiveDir(cwd), 'annotations');
|
||||
}
|
||||
|
||||
export function getCritiqueDir(cwd = process.cwd()) {
|
||||
return path.join(getImpeccableDir(cwd), CRITIQUE_DIR);
|
||||
}
|
||||
|
||||
export function getLegacyLiveAnnotationsDir(cwd = process.cwd()) {
|
||||
return path.join(cwd, '.impeccable-live', 'annotations');
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{
|
||||
"name": "impeccable",
|
||||
"description": "Design fluency for frontend development. 1 skill with 23 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
|
||||
"version": "3.0.7",
|
||||
"version": "3.1.0",
|
||||
"author": {
|
||||
"name": "Paul Bakaus",
|
||||
"email": "paul@paulbakaus.com"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "impeccable",
|
||||
"description": "Design fluency for frontend development. 1 skill with 23 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
|
||||
"version": "3.0.7",
|
||||
"version": "3.1.0",
|
||||
"author": {
|
||||
"name": "Paul Bakaus",
|
||||
"email": "paul@paulbakaus.com"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 3.0.7
|
||||
version: 3.1.0
|
||||
user-invocable: true
|
||||
argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · teach|document|extract|live] [target]"
|
||||
license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution.
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# Codex: Visual Direction & Asset Production
|
||||
|
||||
This file is loaded by `/impeccable craft` when the harness has native image generation (currently Codex via `image_gen`). Other harnesses skip it. It covers the two craft steps that depend on real image generation: landing the visual direction, and producing the raster assets the implementation will compose.
|
||||
|
||||
Read this *before* generating any images. The order matters, and the per-step user pauses are what keep generated imagery from drifting away from the brief.
|
||||
|
||||
## Step A: Explore Directions with the User
|
||||
|
||||
Before generating anything, run a brief direction conversation grounded in the shape brief.
|
||||
|
||||
Ask **2-3 targeted questions** about visual lane, color strategy, atmosphere, and named anchor references. Don't enumerate generic menus; tie each question to the shape brief's answers. Example shape-grounded questions:
|
||||
|
||||
- "Brief says 'editorial restraint, Klim-adjacent.' Are we closer to a quiet specimen page or a magazine-spread feel with hero imagery?"
|
||||
- "Palette strategy from shape was 'Committed.' Want it warm-grounded (deep oxblood + cream) or cool-grounded (slate + paper white)?"
|
||||
|
||||
Stop and wait for answers. These pin the palette before any pixel gets generated.
|
||||
|
||||
## Step B: Generate the Brand Palette First
|
||||
|
||||
Generate **one** palette artifact before any mocks. This is a small, focused image: typography pairing on the chosen background, primary + accent color swatches, one signature ornament or motif. Single image, single pass.
|
||||
|
||||
Why palette first: mocks generated against a vague color sense produce noise that drowns out the structural decisions. A confirmed palette is the first concrete contract for everything downstream.
|
||||
|
||||
Show the palette to the user. Ask one question: "This is the palette I'm locking in for the mocks. Confirm, or call out what to shift?"
|
||||
|
||||
Wait for confirmation. Do not generate mocks against an unconfirmed palette.
|
||||
|
||||
## Step C: Generate 1-3 Visual Mocks Against the Palette
|
||||
|
||||
Once the palette is confirmed, generate **1 to 3** high-fidelity north-star comps. Each mock must use the confirmed palette and typography. Mocks differ in *structural* direction (hierarchy, topology, density, composition), not in color or motif.
|
||||
|
||||
- Brand work: push visual identity, composition, mood, and signature motifs.
|
||||
- Product work: push hierarchy, topology, density, tone, grounded in realistic product structure.
|
||||
- Landing pages and long-form brand surfaces: show enough of the second fold to establish the system beyond the hero.
|
||||
|
||||
Use the `image_gen` tool directly (or via the imagegen skill when available). Don't ask the user to install anything.
|
||||
|
||||
## Step D: Approval Loop
|
||||
|
||||
Show the comps. Ask what carries forward. Iterate until **one direction is approved** or the user explicitly delegates.
|
||||
|
||||
If the user delegates, pick the strongest direction and explain it from the brief, not personal taste.
|
||||
|
||||
Before moving to assets, summarize what to carry into code and what *not* to literalize from the mock. This is the handoff between visual exploration and semantic implementation.
|
||||
|
||||
## Step E: Mock Fidelity Inventory
|
||||
|
||||
Inventory the approved mock's major visible ingredients. For each, decide implementation: semantic HTML/CSS/SVG, generated raster, sourced raster, icon library, canvas/WebGL, or accepted omission.
|
||||
|
||||
Common ingredients to inventory:
|
||||
|
||||
- Hero silhouette and dominant composition
|
||||
- Signature motifs (planets, devices, portraits, charts, route lines, insets, badges, etc.)
|
||||
- Nav and primary CTA treatment
|
||||
- Section sequence, especially the second fold
|
||||
- Image-native content the concept depends on
|
||||
- Typography, density, color/material treatment, motion cues
|
||||
|
||||
Treat the mock as a north star, not a screenshot to trace. Don't rasterize core UI text. But if the live result lacks the mock's major ingredients, the implementation is wrong.
|
||||
|
||||
If a photographic, architectural, product, or place-led mock becomes generic CSS scenery, decorative diagrams, bullets, or copy, stop and fix it. That's a broken implementation, not a harmless interpretation.
|
||||
|
||||
Don't substitute a different hero composition or visual driver post-approval without user sign-off.
|
||||
|
||||
## Step F: Asset Slicing via the Asset Producer
|
||||
|
||||
Raster ingredients identified in Step E need clean production assets. Use the bundled `impeccable_asset_producer` subagent rather than producing inline.
|
||||
|
||||
Spawn it as a scoped subagent. If you do not have explicit permission to use agents, stop and ask:
|
||||
|
||||
```text
|
||||
Asset production will work better as a scoped subagent job. Should I spawn the Impeccable asset producer subagent for this step?
|
||||
```
|
||||
|
||||
Pass to the agent:
|
||||
|
||||
- Approved mock path or screenshot reference
|
||||
- Crop paths or a contact sheet with crop ids
|
||||
- Output directory
|
||||
- Required dimensions, format, transparency needs
|
||||
- Avoid list
|
||||
- Notes on what should remain semantic HTML/CSS/SVG instead of raster
|
||||
|
||||
Attach image generation capability to the spawned agent when the harness supports it. Do **not** load image-generation reference material into the parent thread.
|
||||
|
||||
Inline asset production is allowed only if the user declines subagents, the harness cannot spawn the authorized agent, or the user explicitly asks for single-thread mode.
|
||||
|
||||
Prefer HTML/CSS/SVG/canvas when they can credibly reproduce an ingredient; reach for real, generated, or stock imagery when the mock or subject matter calls for actual visual content.
|
||||
|
||||
## After This File
|
||||
|
||||
Once Steps A through F are complete, return to `craft.md` Step 5 (Build to Production Quality). The implementation builds against the confirmed palette, approved mock, and the assets the producer wrote.
|
||||
@@ -51,64 +51,15 @@ Then add references based on the brief's needs:
|
||||
- Responsive requirements? Consult [responsive-design.md](responsive-design.md)
|
||||
- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.md)
|
||||
|
||||
## Step 3: Land the Visual Direction (Capability-Gated)
|
||||
## Step 3: Visual Direction & Assets (Harness-Gated)
|
||||
|
||||
Generate high-fidelity visual comps before implementation when all three are true:
|
||||
If the harness has **native image generation** (currently Codex via `image_gen`), this step is mandatory. **Stop and load [codex.md](codex.md)**. It covers palette generation, mock exploration, the approval loop, mock-fidelity inventory, and asset slicing via the `impeccable_asset_producer` subagent. Follow Steps A-F in that file, then return here for Step 4.
|
||||
|
||||
- The work is net-new or visually open-ended enough that composition exploration will improve the build.
|
||||
- The brief's scope is mid-fi, high-fi, or production-ready.
|
||||
- The harness gives you native image generation (Codex's `image_gen`, an equivalent MCP tool). Don't ask the user to set up external APIs.
|
||||
If the harness lacks native image generation, **state in one line that the visual-direction-by-generation step is being skipped because the harness lacks native image generation, then proceed**. The one-line announcement is required; it forces a conscious decision instead of letting the step quietly evaporate. The brief is your only visual reference. Implement directly from it, treating any named anchor references and the brief's "Design Direction" as the contract.
|
||||
|
||||
When met, this step is mandatory for both brand and product work. If image generation isn't available, skip silently. "The eventual UI is semantic/code-native/accessible" is not a reason to skip; those are implementation requirements, not exploration ones.
|
||||
Whether you generated mocks or not: don't replace required imagery with generic cards, bullets, emoji, fake metrics, decorative CSS panels, or filler copy. Image-led briefs (restaurants, hotels, magazines, photography, hobbyist communities, food, travel, fashion, product) need real or sourced imagery in the build, not CSS scenery.
|
||||
|
||||
### What to generate
|
||||
|
||||
Generate **1 to 3** high-fidelity north-star comps from the confirmed brief. If shape already produced direction probes, resolve the winning lane further, not an unrelated exploration. Comps must differ in primary visual direction, not just color.
|
||||
|
||||
- Brand work: push visual identity, composition, and mood aggressively.
|
||||
- Product work: push hierarchy, topology, density, tone, grounded in realistic product structure.
|
||||
- Landing pages and long-form brand surfaces: show enough of the second fold to establish the system beyond the hero.
|
||||
|
||||
### Approval loop
|
||||
|
||||
Show the comps and ask what carries forward. Iterate until one direction is approved or the user delegates. If the user delegates, pick the strongest direction and explain it from the brief, not personal taste.
|
||||
|
||||
Before Step 4, summarize what to carry into code and what **not** to literalize from the mock. This is the handoff between visual exploration and semantic implementation.
|
||||
|
||||
### Mock fidelity inventory
|
||||
|
||||
Inventory the approved mock's major visible ingredients:
|
||||
|
||||
- Hero silhouette and dominant composition
|
||||
- Signature motifs (planets, devices, portraits, charts, route lines, insets, badges, etc.)
|
||||
- Nav and primary CTA treatment
|
||||
- Section sequence, especially the second fold
|
||||
- Image-native content the concept depends on
|
||||
- Typography, density, color/material treatment, motion cues
|
||||
|
||||
For each, decide implementation: semantic HTML/CSS/SVG, generated asset, sourced asset, icon library, canvas/WebGL, or accepted omission. Don't substitute a different hero composition or visual driver post-approval without user sign-off.
|
||||
|
||||
If a photographic, architectural, product, or place-led mock becomes generic CSS scenery / decorative diagrams / bullets / copy, stop and fix it. That's a broken implementation, not a harmless interpretation.
|
||||
|
||||
Treat the mock as a north star, not a screenshot to trace. Don't rasterize core UI text. But if the live result lacks the mock's major ingredients, the implementation is wrong.
|
||||
|
||||
## Step 4: Asset Extraction (Need-Gated)
|
||||
|
||||
If the approved direction needs raster assets, create them before building. Do not replace required imagery with generic cards, bullets, emoji, fake metrics, decorative CSS panels, or filler copy.
|
||||
|
||||
Use the native asset producer (`impeccable_asset_producer` in Codex, `impeccable-asset-producer` in Claude Code) to create clean assets from the hi-fi mock and crops. If you do not have explicit permission to use agents, stop and ask:
|
||||
|
||||
```text
|
||||
Asset production will work better as a scoped subagent job. Should I spawn the Impeccable asset producer subagent for this step?
|
||||
```
|
||||
|
||||
Do not skip asset production or silently do it inline. Inline asset production is allowed only if the user declines subagents, the harness cannot spawn the authorized agent, or the user explicitly asks for single-thread mode.
|
||||
|
||||
Pass the approved mock, crop/contact-sheet paths, output directory, dimensions/formats, transparency needs, constraints, and avoid list to the asset producer. Attach image generation capability to the spawned agent when the harness supports it; do not load image-generation reference material into the parent thread first.
|
||||
|
||||
Prefer HTML/CSS/SVG/canvas when they can credibly reproduce an ingredient; use real/generated/stock imagery when the mock or subject matter calls for actual visual content.
|
||||
|
||||
## Step 5: Build to Production Quality
|
||||
## Step 4: Build to Production Quality
|
||||
|
||||
Implement the feature following the design brief. Build in passes so structure, visual system, states, motion/media, and responsive behavior each get deliberate attention. The list below is the definition of done, not inspiration.
|
||||
|
||||
@@ -130,7 +81,7 @@ Implement the feature following the design brief. Build in passes so structure,
|
||||
- **Technically clean.** Production build passes, no console errors, no avoidable layout shift, no needless dependencies, no broken asset paths.
|
||||
- **Ask when uncertain.** If a discovery materially changes the brief or approved direction, stop and ask. Don't guess.
|
||||
|
||||
## Step 6: Iterate Visually
|
||||
## Step 5: Iterate Visually
|
||||
|
||||
Look at what you built like a designer would. Your eyes are whatever the harness gives you: a connected browser, a screenshotting tool, Playwright, or asking the user. Use them for responsive testing (mobile, tablet, desktop minimum) and general visual validation.
|
||||
|
||||
@@ -144,7 +95,7 @@ Actively check: responsive behavior (composes, not shrinks), every state (empty
|
||||
|
||||
Detector or QA output is defect evidence only; never proof the work is finished.
|
||||
|
||||
## Step 7: Present
|
||||
## Step 6: Present
|
||||
|
||||
Present the result to the user:
|
||||
- Show the feature in its primary state
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
> **Additional context needed**: what the interface is trying to accomplish.
|
||||
|
||||
### Setup: Resolve Target and Load Ignore List
|
||||
|
||||
Before gathering assessments, do two small bookkeeping steps. They cost almost nothing and they're what makes critique iterative across runs.
|
||||
|
||||
1. **Resolve the primary artifact.** The user's phrasing ("the homepage", "the pricing flow") is not stable enough to track across runs. Resolve it to a concrete file path or URL: the same one you'd already need to scan code or open in a browser. Examples:
|
||||
- "the homepage" → `site/pages/index.astro` (or `http://localhost:3000/` if you're inspecting live)
|
||||
- "the settings modal" → the primary component file (e.g., `src/components/Settings.tsx`)
|
||||
- "this page" → the URL or the page's source file
|
||||
Prefer the source file path over the dev-server URL when both exist; ports drift between runs (`bun dev` vs `bun preview`), file paths don't.
|
||||
|
||||
2. **Compute the slug.** Run:
|
||||
```bash
|
||||
node .claude/skills/impeccable/scripts/critique-storage.mjs slug "<resolved-path-or-url>"
|
||||
```
|
||||
Keep the printed slug. It identifies this target's stream across runs. If the command exits non-zero ("no stable slug for input"), skip persistence for this run and tell the user; the trend won't update but the critique still goes ahead.
|
||||
|
||||
3. **Read the ignore list** at `.impeccable/critique/ignore.md` if it exists. Plain markdown; each non-empty, non-comment line is something the user has marked as "do not re-raise" (deferred tradeoffs, designer-intended deviations, detector false-positives the user accepts). When a finding's text matches a line here (case-insensitive substring against rule name or snippet), **drop it silently**. Do not mention it in the report. This is the ONLY input critique consumes from prior runs; anchoring on prior findings would defeat the point of independent assessment.
|
||||
|
||||
### Gather Assessments
|
||||
|
||||
Launch two independent assessments. **Neither may see the other's output.** This isolation is what makes the combined score honest. Running both in one head silently anchors them to each other; do not shortcut it for cost, speed, or context-size reasons.
|
||||
@@ -164,6 +182,36 @@ Provocative questions that might unlock better solutions:
|
||||
- Prioritize ruthlessly. If everything is important, nothing is.
|
||||
- Don't soften criticism. Developers need honest feedback to ship great design.
|
||||
|
||||
### Persist the Snapshot
|
||||
|
||||
Once the report above is finalized, write it to `.impeccable/critique/` so the user can refer back, and so `/impeccable polish` can pick up the priority issues without a copy-paste.
|
||||
|
||||
Skip this step if the Setup slug was null (vague or root-level target).
|
||||
|
||||
1. **Write the body to a temp file** so you can pipe it to the helper. Use the full report (heuristic table, anti-patterns verdict, priority issues, persona red flags) but stop before the "Ask the User" / "Recommended Actions" sections that come later.
|
||||
|
||||
2. **Pass the structured metadata** through `IMPECCABLE_CRITIQUE_META` (JSON), then run the write command:
|
||||
```bash
|
||||
IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"p0_count":<n>,"p1_count":<n>}' \
|
||||
node .claude/skills/impeccable/scripts/critique-storage.mjs write <slug> <body-file>
|
||||
```
|
||||
The helper prints the absolute path it wrote.
|
||||
|
||||
3. **Read the trend** for context:
|
||||
```bash
|
||||
node .claude/skills/impeccable/scripts/critique-storage.mjs trend <slug> 5
|
||||
```
|
||||
This returns a JSON array of the last 5 frontmatter entries (including the one you just wrote).
|
||||
|
||||
4. **Append a single line to the user-visible output**, after the report and before the questions:
|
||||
|
||||
> **Trend for `<slug>` (last 5 runs): 24 → 28 → 32 → 29 → 32**
|
||||
> Wrote `.impeccable/critique/<filename>`.
|
||||
|
||||
If this is the first run for the slug, the trend is just one score; say so: "First run for this target, no trend yet."
|
||||
|
||||
This is fire-and-forget. Do not show the user the helper's JSON output; only the human-readable trend line and the written path. Failures here should not block the rest of the flow; print the error and move on.
|
||||
|
||||
### Ask the User
|
||||
|
||||
**After presenting findings**, use targeted questions based on what was actually found. STOP and call the AskUserQuestion tool to clarify. These answers will shape the action plan.
|
||||
|
||||
@@ -35,7 +35,14 @@ Understand the current state and goals before touching anything:
|
||||
- Loading and transition smoothness
|
||||
- Information architecture and flow drift (does this feature reveal complexity the way neighboring features do?)
|
||||
|
||||
4. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent; never perfect one corner while leaving another rough.
|
||||
4. **Pull in any prior critique** (optional signal): If `/impeccable critique` has been run on the same target, its priority issues are a useful prior for what to address first. Resolve the target to a file path or URL, then:
|
||||
```bash
|
||||
slug=$(node .claude/skills/impeccable/scripts/critique-storage.mjs slug "<resolved>")
|
||||
node .claude/skills/impeccable/scripts/critique-storage.mjs latest "$slug"
|
||||
```
|
||||
Exit 0 with body = found; fold the P0/P1 items into your polish list and mention the snapshot path so the user sees what you read. Exit 2 = no snapshot, continue without it. The critique is one input among many. Do your own pass either way.
|
||||
|
||||
5. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent; never perfect one corner while leaving another rough.
|
||||
|
||||
**CRITICAL**: Polish is the last step, not the first. Don't polish work that's not functionally complete.
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ After the discovery interview, generate a small set of visual direction probes *
|
||||
- The requested fidelity is **mid-fi, high-fi, or production-ready**. Skip for sketch-only planning.
|
||||
- The current harness gives you native image generation (Codex's `image_gen`, an equivalent MCP tool, or similar). Don't ask the user to install APIs or tooling.
|
||||
|
||||
When those conditions are met, this step is mandatory. If image generation isn't natively available, skip silently and proceed; don't announce the skip.
|
||||
When those conditions are met, this step is mandatory. If image generation isn't natively available, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed. The one-line announcement is required, not optional; it forces a conscious decision instead of letting the step quietly evaporate.
|
||||
|
||||
Use probes to explore visual lanes, not to replace the brief.
|
||||
|
||||
@@ -107,7 +107,7 @@ The probes should differ in primary visual direction (hierarchy, topology, densi
|
||||
- Do **not** treat generated imagery as final UX specification, final copy, or final accessibility behavior.
|
||||
- Do **not** use this step for minor refinements of existing work. It's for shaping a new surface or clarifying a big directional choice.
|
||||
|
||||
If image generation isn't natively available, skip this phase silently and proceed.
|
||||
If image generation isn't natively available, announce the skip in one line and proceed to the design brief.
|
||||
|
||||
## Phase 2: Design Brief
|
||||
|
||||
@@ -120,7 +120,7 @@ After the interview and any required probes, present a brief and **end your resp
|
||||
|
||||
Don't pad a clear brief into a long one to look thorough. A 70-line brief restating answers the user just gave is noise, not rigor. Equally, don't skip the confirmation pause to look efficient: the pause is the point.
|
||||
|
||||
If the user already said "approved" or "go" during discovery for the *exact direction you'd present*, that counts as confirmation; you may proceed without asking again. But if your brief adds anything the user hasn't seen and approved, you must stop and confirm.
|
||||
Present the brief, then **stop and wait for explicit confirmation**. You are not the judge of whether the user already approved. Even when the brief feels obviously right, ask once and wait. The pause is what separates shape from premature implementation.
|
||||
|
||||
### Brief Structure
|
||||
|
||||
@@ -158,8 +158,8 @@ Anything genuinely unresolved. Don't list "open questions" you've already recomm
|
||||
|
||||
---
|
||||
|
||||
STOP and call the AskUserQuestion tool to clarify.
|
||||
STOP and call the AskUserQuestion tool to clarify. Ask for explicit confirmation of the brief before finishing.
|
||||
|
||||
If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the user confirms direction (or the user already gave a clear go during discovery, which counts).
|
||||
If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the user confirms direction.
|
||||
|
||||
Once confirmed, the brief is complete. The user can now hand it to /impeccable, or use it to guide any other implementation approach. (If the user wants the full discovery-then-build flow in one step, they should use /impeccable craft instead, which runs this command internally.)
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Critique persistence helper.
|
||||
*
|
||||
* Each run of /impeccable critique writes a per-target snapshot to
|
||||
* .impeccable/critique/<timestamp>__<slug>.md
|
||||
* with a small YAML frontmatter carrying the score + P0/P1 counts.
|
||||
*
|
||||
* /impeccable polish reads the latest matching snapshot at start as its
|
||||
* fix backlog. No other skill auto-reads critique output.
|
||||
*
|
||||
* The slug is derived mechanically from the *resolved* primary artifact
|
||||
* (file path or URL), never from the user's natural-language phrasing.
|
||||
* Slug stability across runs is what lets the trend display work.
|
||||
*
|
||||
* CLI entry points (called from skill instructions):
|
||||
* node critique-storage.mjs slug <resolved-target>
|
||||
* node critique-storage.mjs write <slug> <snapshot-body-file>
|
||||
* node critique-storage.mjs latest <slug>
|
||||
* node critique-storage.mjs trend <slug> [limit]
|
||||
*
|
||||
* Note: there is intentionally no `ignore` subcommand. ignore.md is a plain
|
||||
* markdown file; the model reads it directly with its file-read tool. This
|
||||
* helper only exists for operations the model can't trivially do inline
|
||||
* (normalizing paths, generating filenames, globbing + parsing frontmatter).
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { getCritiqueDir } from './impeccable-paths.mjs';
|
||||
|
||||
const SLUG_MAX = 50;
|
||||
|
||||
/**
|
||||
* Mechanically derive a slug from a resolved target. Returns null if the
|
||||
* input doesn't look like a stable identifier (empty, project root, etc).
|
||||
*
|
||||
* Accepts file paths and URLs. The model resolves "the homepage" to a
|
||||
* concrete artifact before calling this — we never slug a natural-language
|
||||
* phrase.
|
||||
*/
|
||||
export function slugFromTarget(resolved, { cwd = process.cwd() } = {}) {
|
||||
if (!resolved || typeof resolved !== 'string') return null;
|
||||
const trimmed = resolved.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
// URL
|
||||
if (/^https?:\/\//i.test(trimmed)) {
|
||||
let url;
|
||||
try { url = new URL(trimmed); } catch { return null; }
|
||||
const hostPath = `${url.hostname}${url.pathname}`;
|
||||
return kebab(hostPath);
|
||||
}
|
||||
|
||||
// File path. Make it project-relative so two devs critiquing the same
|
||||
// checkout get the same slug regardless of where their repo is cloned.
|
||||
const abs = path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
|
||||
let rel = path.relative(cwd, abs);
|
||||
// If the target is outside cwd, fall back to the basename so we still
|
||||
// produce a stable slug (vs the absolute path, which would include
|
||||
// home dirs / usernames).
|
||||
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
||||
rel = path.basename(abs);
|
||||
}
|
||||
if (!rel || rel === '.' || rel === '') return null;
|
||||
return kebab(rel);
|
||||
}
|
||||
|
||||
function kebab(s) {
|
||||
const slug = s
|
||||
.toLowerCase()
|
||||
.replace(/[/\\.]+/g, '-')
|
||||
.replace(/[^a-z0-9-]+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
if (!slug) return null;
|
||||
// Cap from the tail — the tail (filename) is more identifying than the
|
||||
// top-level directory.
|
||||
return slug.length <= SLUG_MAX ? slug : slug.slice(slug.length - SLUG_MAX).replace(/^-/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filename-safe UTC ISO timestamp: hyphens for separators, trailing Z.
|
||||
* Plain colons aren't allowed on Windows filesystems.
|
||||
*/
|
||||
export function nowFilenameStamp(date = new Date()) {
|
||||
const iso = date.toISOString(); // 2026-05-12T18:30:00.123Z
|
||||
return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z');
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a snapshot for `slug`. `meta` carries the small structured frontmatter
|
||||
* keys read back by readTrend(). `body` is the human-readable critique
|
||||
* report (everything below the frontmatter).
|
||||
*
|
||||
* Returns the absolute path written.
|
||||
*/
|
||||
export function writeSnapshot({ slug, meta, body, cwd = process.cwd(), now = new Date() }) {
|
||||
if (!slug) throw new Error('writeSnapshot requires a slug');
|
||||
const dir = getCritiqueDir(cwd);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const timestamp = nowFilenameStamp(now);
|
||||
const filePath = path.join(dir, `${timestamp}__${slug}.md`);
|
||||
// Spread `meta` first so internally computed `timestamp` and `slug`
|
||||
// always win. Otherwise a caller-supplied meta blob (parsed from the
|
||||
// IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the
|
||||
// filename in disagreement with its frontmatter and corrupting trends.
|
||||
const front = serializeFrontmatter({ ...meta, timestamp, slug });
|
||||
fs.writeFileSync(filePath, `${front}\n${body.trim()}\n`, 'utf-8');
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function serializeFrontmatter(obj) {
|
||||
const lines = ['---'];
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
const str = typeof value === 'string' ? value : String(value);
|
||||
// Quote strings that contain : or # to keep parsing simple.
|
||||
const needsQuotes = typeof value === 'string' && /[:#]/.test(str);
|
||||
lines.push(`${key}: ${needsQuotes ? JSON.stringify(str) : str}`);
|
||||
}
|
||||
lines.push('---');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function parseFrontmatter(text) {
|
||||
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
if (!match) return {};
|
||||
const out = {};
|
||||
for (const line of match[1].split(/\r?\n/)) {
|
||||
const colon = line.indexOf(':');
|
||||
if (colon < 0) continue;
|
||||
const key = line.slice(0, colon).trim();
|
||||
let value = line.slice(colon + 1).trim();
|
||||
if (/^".*"$/.test(value)) {
|
||||
try { value = JSON.parse(value); } catch { /* leave as-is */ }
|
||||
} else if (/^-?\d+$/.test(value)) {
|
||||
value = Number(value);
|
||||
}
|
||||
out[key] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all snapshot files for `slug`, sorted oldest → newest.
|
||||
*/
|
||||
function listSnapshotsForSlug(slug, cwd) {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const suffix = `__${slug}.md`;
|
||||
return fs.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(suffix))
|
||||
.sort()
|
||||
.map((f) => path.join(dir, f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent snapshot for `slug`, or null. Polish reads this
|
||||
* to find its fix backlog when the slug matches.
|
||||
*/
|
||||
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
if (!all.length) return null;
|
||||
const latest = all[all.length - 1];
|
||||
const body = fs.readFileSync(latest, 'utf-8');
|
||||
return { path: latest, body, meta: parseFrontmatter(body) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last `limit` snapshots' frontmatter, oldest → newest.
|
||||
* Critique appends a one-line trend to its output using this.
|
||||
*/
|
||||
export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
const slice = all.slice(-limit);
|
||||
return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8')));
|
||||
}
|
||||
|
||||
// ---- CLI ---------------------------------------------------------------
|
||||
|
||||
function main(argv) {
|
||||
const [cmd, ...args] = argv;
|
||||
switch (cmd) {
|
||||
case 'slug': {
|
||||
const slug = slugFromTarget(args[0]);
|
||||
if (!slug) { process.stderr.write('no stable slug for input\n'); process.exit(1); }
|
||||
process.stdout.write(`${slug}\n`);
|
||||
return;
|
||||
}
|
||||
case 'write': {
|
||||
const [slug, bodyFile] = args;
|
||||
if (!slug || !bodyFile) { process.stderr.write('usage: write <slug> <body-file>\n'); process.exit(1); }
|
||||
const raw = fs.readFileSync(bodyFile, 'utf-8');
|
||||
// The body file may be a full report. The caller passes the meta as
|
||||
// a JSON object on stdin if it wants structured frontmatter; otherwise
|
||||
// we write with minimal metadata.
|
||||
let meta = {};
|
||||
const metaArg = process.env.IMPECCABLE_CRITIQUE_META;
|
||||
if (metaArg) {
|
||||
try { meta = JSON.parse(metaArg); } catch { /* ignore */ }
|
||||
}
|
||||
const out = writeSnapshot({ slug, meta, body: raw });
|
||||
process.stdout.write(`${out}\n`);
|
||||
return;
|
||||
}
|
||||
case 'latest': {
|
||||
const latest = readLatestSnapshot(args[0]);
|
||||
if (!latest) { process.exit(2); }
|
||||
process.stdout.write(latest.body);
|
||||
return;
|
||||
}
|
||||
case 'trend': {
|
||||
const rows = readTrend(args[0], { limit: args[1] ? Number(args[1]) : 5 });
|
||||
process.stdout.write(JSON.stringify(rows, null, 2) + '\n');
|
||||
return;
|
||||
}
|
||||
default:
|
||||
process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend> [args]\n');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main(process.argv.slice(2));
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import path from 'node:path';
|
||||
|
||||
export const IMPECCABLE_DIR = '.impeccable';
|
||||
export const LIVE_DIR = 'live';
|
||||
export const CRITIQUE_DIR = 'critique';
|
||||
|
||||
export function getImpeccableDir(cwd = process.cwd()) {
|
||||
return path.join(cwd, IMPECCABLE_DIR);
|
||||
@@ -96,6 +97,10 @@ export function getLiveAnnotationsDir(cwd = process.cwd()) {
|
||||
return path.join(getLiveDir(cwd), 'annotations');
|
||||
}
|
||||
|
||||
export function getCritiqueDir(cwd = process.cwd()) {
|
||||
return path.join(getImpeccableDir(cwd), CRITIQUE_DIR);
|
||||
}
|
||||
|
||||
export function getLegacyLiveAnnotationsDir(cwd = process.cwd()) {
|
||||
return path.join(cwd, '.impeccable-live', 'annotations');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 3.0.7
|
||||
version: 3.1.0
|
||||
license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution.
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# Codex: Visual Direction & Asset Production
|
||||
|
||||
This file is loaded by `/impeccable craft` when the harness has native image generation (currently Codex via `image_gen`). Other harnesses skip it. It covers the two craft steps that depend on real image generation: landing the visual direction, and producing the raster assets the implementation will compose.
|
||||
|
||||
Read this *before* generating any images. The order matters, and the per-step user pauses are what keep generated imagery from drifting away from the brief.
|
||||
|
||||
## Step A: Explore Directions with the User
|
||||
|
||||
Before generating anything, run a brief direction conversation grounded in the shape brief.
|
||||
|
||||
Ask **2-3 targeted questions** about visual lane, color strategy, atmosphere, and named anchor references. Don't enumerate generic menus; tie each question to the shape brief's answers. Example shape-grounded questions:
|
||||
|
||||
- "Brief says 'editorial restraint, Klim-adjacent.' Are we closer to a quiet specimen page or a magazine-spread feel with hero imagery?"
|
||||
- "Palette strategy from shape was 'Committed.' Want it warm-grounded (deep oxblood + cream) or cool-grounded (slate + paper white)?"
|
||||
|
||||
Stop and wait for answers. These pin the palette before any pixel gets generated.
|
||||
|
||||
## Step B: Generate the Brand Palette First
|
||||
|
||||
Generate **one** palette artifact before any mocks. This is a small, focused image: typography pairing on the chosen background, primary + accent color swatches, one signature ornament or motif. Single image, single pass.
|
||||
|
||||
Why palette first: mocks generated against a vague color sense produce noise that drowns out the structural decisions. A confirmed palette is the first concrete contract for everything downstream.
|
||||
|
||||
Show the palette to the user. Ask one question: "This is the palette I'm locking in for the mocks. Confirm, or call out what to shift?"
|
||||
|
||||
Wait for confirmation. Do not generate mocks against an unconfirmed palette.
|
||||
|
||||
## Step C: Generate 1-3 Visual Mocks Against the Palette
|
||||
|
||||
Once the palette is confirmed, generate **1 to 3** high-fidelity north-star comps. Each mock must use the confirmed palette and typography. Mocks differ in *structural* direction (hierarchy, topology, density, composition), not in color or motif.
|
||||
|
||||
- Brand work: push visual identity, composition, mood, and signature motifs.
|
||||
- Product work: push hierarchy, topology, density, tone, grounded in realistic product structure.
|
||||
- Landing pages and long-form brand surfaces: show enough of the second fold to establish the system beyond the hero.
|
||||
|
||||
Use the `image_gen` tool directly (or via the imagegen skill when available). Don't ask the user to install anything.
|
||||
|
||||
## Step D: Approval Loop
|
||||
|
||||
Show the comps. Ask what carries forward. Iterate until **one direction is approved** or the user explicitly delegates.
|
||||
|
||||
If the user delegates, pick the strongest direction and explain it from the brief, not personal taste.
|
||||
|
||||
Before moving to assets, summarize what to carry into code and what *not* to literalize from the mock. This is the handoff between visual exploration and semantic implementation.
|
||||
|
||||
## Step E: Mock Fidelity Inventory
|
||||
|
||||
Inventory the approved mock's major visible ingredients. For each, decide implementation: semantic HTML/CSS/SVG, generated raster, sourced raster, icon library, canvas/WebGL, or accepted omission.
|
||||
|
||||
Common ingredients to inventory:
|
||||
|
||||
- Hero silhouette and dominant composition
|
||||
- Signature motifs (planets, devices, portraits, charts, route lines, insets, badges, etc.)
|
||||
- Nav and primary CTA treatment
|
||||
- Section sequence, especially the second fold
|
||||
- Image-native content the concept depends on
|
||||
- Typography, density, color/material treatment, motion cues
|
||||
|
||||
Treat the mock as a north star, not a screenshot to trace. Don't rasterize core UI text. But if the live result lacks the mock's major ingredients, the implementation is wrong.
|
||||
|
||||
If a photographic, architectural, product, or place-led mock becomes generic CSS scenery, decorative diagrams, bullets, or copy, stop and fix it. That's a broken implementation, not a harmless interpretation.
|
||||
|
||||
Don't substitute a different hero composition or visual driver post-approval without user sign-off.
|
||||
|
||||
## Step F: Asset Slicing via the Asset Producer
|
||||
|
||||
Raster ingredients identified in Step E need clean production assets. Use the bundled `impeccable_asset_producer` subagent rather than producing inline.
|
||||
|
||||
Spawn it as a scoped subagent. If you do not have explicit permission to use agents, stop and ask:
|
||||
|
||||
```text
|
||||
Asset production will work better as a scoped subagent job. Should I spawn the Impeccable asset producer subagent for this step?
|
||||
```
|
||||
|
||||
Pass to the agent:
|
||||
|
||||
- Approved mock path or screenshot reference
|
||||
- Crop paths or a contact sheet with crop ids
|
||||
- Output directory
|
||||
- Required dimensions, format, transparency needs
|
||||
- Avoid list
|
||||
- Notes on what should remain semantic HTML/CSS/SVG instead of raster
|
||||
|
||||
Attach image generation capability to the spawned agent when the harness supports it. Do **not** load image-generation reference material into the parent thread.
|
||||
|
||||
Inline asset production is allowed only if the user declines subagents, the harness cannot spawn the authorized agent, or the user explicitly asks for single-thread mode.
|
||||
|
||||
Prefer HTML/CSS/SVG/canvas when they can credibly reproduce an ingredient; reach for real, generated, or stock imagery when the mock or subject matter calls for actual visual content.
|
||||
|
||||
## After This File
|
||||
|
||||
Once Steps A through F are complete, return to `craft.md` Step 5 (Build to Production Quality). The implementation builds against the confirmed palette, approved mock, and the assets the producer wrote.
|
||||
@@ -51,64 +51,15 @@ Then add references based on the brief's needs:
|
||||
- Responsive requirements? Consult [responsive-design.md](responsive-design.md)
|
||||
- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.md)
|
||||
|
||||
## Step 3: Land the Visual Direction (Capability-Gated)
|
||||
## Step 3: Visual Direction & Assets (Harness-Gated)
|
||||
|
||||
Generate high-fidelity visual comps before implementation when all three are true:
|
||||
If the harness has **native image generation** (currently Codex via `image_gen`), this step is mandatory. **Stop and load [codex.md](codex.md)**. It covers palette generation, mock exploration, the approval loop, mock-fidelity inventory, and asset slicing via the `impeccable_asset_producer` subagent. Follow Steps A-F in that file, then return here for Step 4.
|
||||
|
||||
- The work is net-new or visually open-ended enough that composition exploration will improve the build.
|
||||
- The brief's scope is mid-fi, high-fi, or production-ready.
|
||||
- The harness gives you native image generation (Codex's `image_gen`, an equivalent MCP tool). Don't ask the user to set up external APIs.
|
||||
If the harness lacks native image generation, **state in one line that the visual-direction-by-generation step is being skipped because the harness lacks native image generation, then proceed**. The one-line announcement is required; it forces a conscious decision instead of letting the step quietly evaporate. The brief is your only visual reference. Implement directly from it, treating any named anchor references and the brief's "Design Direction" as the contract.
|
||||
|
||||
When met, this step is mandatory for both brand and product work. If image generation isn't available, skip silently. "The eventual UI is semantic/code-native/accessible" is not a reason to skip; those are implementation requirements, not exploration ones.
|
||||
Whether you generated mocks or not: don't replace required imagery with generic cards, bullets, emoji, fake metrics, decorative CSS panels, or filler copy. Image-led briefs (restaurants, hotels, magazines, photography, hobbyist communities, food, travel, fashion, product) need real or sourced imagery in the build, not CSS scenery.
|
||||
|
||||
### What to generate
|
||||
|
||||
Generate **1 to 3** high-fidelity north-star comps from the confirmed brief. If shape already produced direction probes, resolve the winning lane further, not an unrelated exploration. Comps must differ in primary visual direction, not just color.
|
||||
|
||||
- Brand work: push visual identity, composition, and mood aggressively.
|
||||
- Product work: push hierarchy, topology, density, tone, grounded in realistic product structure.
|
||||
- Landing pages and long-form brand surfaces: show enough of the second fold to establish the system beyond the hero.
|
||||
|
||||
### Approval loop
|
||||
|
||||
Show the comps and ask what carries forward. Iterate until one direction is approved or the user delegates. If the user delegates, pick the strongest direction and explain it from the brief, not personal taste.
|
||||
|
||||
Before Step 4, summarize what to carry into code and what **not** to literalize from the mock. This is the handoff between visual exploration and semantic implementation.
|
||||
|
||||
### Mock fidelity inventory
|
||||
|
||||
Inventory the approved mock's major visible ingredients:
|
||||
|
||||
- Hero silhouette and dominant composition
|
||||
- Signature motifs (planets, devices, portraits, charts, route lines, insets, badges, etc.)
|
||||
- Nav and primary CTA treatment
|
||||
- Section sequence, especially the second fold
|
||||
- Image-native content the concept depends on
|
||||
- Typography, density, color/material treatment, motion cues
|
||||
|
||||
For each, decide implementation: semantic HTML/CSS/SVG, generated asset, sourced asset, icon library, canvas/WebGL, or accepted omission. Don't substitute a different hero composition or visual driver post-approval without user sign-off.
|
||||
|
||||
If a photographic, architectural, product, or place-led mock becomes generic CSS scenery / decorative diagrams / bullets / copy, stop and fix it. That's a broken implementation, not a harmless interpretation.
|
||||
|
||||
Treat the mock as a north star, not a screenshot to trace. Don't rasterize core UI text. But if the live result lacks the mock's major ingredients, the implementation is wrong.
|
||||
|
||||
## Step 4: Asset Extraction (Need-Gated)
|
||||
|
||||
If the approved direction needs raster assets, create them before building. Do not replace required imagery with generic cards, bullets, emoji, fake metrics, decorative CSS panels, or filler copy.
|
||||
|
||||
Use the native asset producer (`impeccable_asset_producer` in Codex, `impeccable-asset-producer` in Claude Code) to create clean assets from the hi-fi mock and crops. If you do not have explicit permission to use agents, stop and ask:
|
||||
|
||||
```text
|
||||
Asset production will work better as a scoped subagent job. Should I spawn the Impeccable asset producer subagent for this step?
|
||||
```
|
||||
|
||||
Do not skip asset production or silently do it inline. Inline asset production is allowed only if the user declines subagents, the harness cannot spawn the authorized agent, or the user explicitly asks for single-thread mode.
|
||||
|
||||
Pass the approved mock, crop/contact-sheet paths, output directory, dimensions/formats, transparency needs, constraints, and avoid list to the asset producer. Attach image generation capability to the spawned agent when the harness supports it; do not load image-generation reference material into the parent thread first.
|
||||
|
||||
Prefer HTML/CSS/SVG/canvas when they can credibly reproduce an ingredient; use real/generated/stock imagery when the mock or subject matter calls for actual visual content.
|
||||
|
||||
## Step 5: Build to Production Quality
|
||||
## Step 4: Build to Production Quality
|
||||
|
||||
Implement the feature following the design brief. Build in passes so structure, visual system, states, motion/media, and responsive behavior each get deliberate attention. The list below is the definition of done, not inspiration.
|
||||
|
||||
@@ -130,7 +81,7 @@ Implement the feature following the design brief. Build in passes so structure,
|
||||
- **Technically clean.** Production build passes, no console errors, no avoidable layout shift, no needless dependencies, no broken asset paths.
|
||||
- **Ask when uncertain.** If a discovery materially changes the brief or approved direction, stop and ask. Don't guess.
|
||||
|
||||
## Step 6: Iterate Visually
|
||||
## Step 5: Iterate Visually
|
||||
|
||||
Look at what you built like a designer would. Your eyes are whatever the harness gives you: a connected browser, a screenshotting tool, Playwright, or asking the user. Use them for responsive testing (mobile, tablet, desktop minimum) and general visual validation.
|
||||
|
||||
@@ -144,7 +95,7 @@ Actively check: responsive behavior (composes, not shrinks), every state (empty
|
||||
|
||||
Detector or QA output is defect evidence only; never proof the work is finished.
|
||||
|
||||
## Step 7: Present
|
||||
## Step 6: Present
|
||||
|
||||
Present the result to the user:
|
||||
- Show the feature in its primary state
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
> **Additional context needed**: what the interface is trying to accomplish.
|
||||
|
||||
### Setup: Resolve Target and Load Ignore List
|
||||
|
||||
Before gathering assessments, do two small bookkeeping steps. They cost almost nothing and they're what makes critique iterative across runs.
|
||||
|
||||
1. **Resolve the primary artifact.** The user's phrasing ("the homepage", "the pricing flow") is not stable enough to track across runs. Resolve it to a concrete file path or URL: the same one you'd already need to scan code or open in a browser. Examples:
|
||||
- "the homepage" → `site/pages/index.astro` (or `http://localhost:3000/` if you're inspecting live)
|
||||
- "the settings modal" → the primary component file (e.g., `src/components/Settings.tsx`)
|
||||
- "this page" → the URL or the page's source file
|
||||
Prefer the source file path over the dev-server URL when both exist; ports drift between runs (`bun dev` vs `bun preview`), file paths don't.
|
||||
|
||||
2. **Compute the slug.** Run:
|
||||
```bash
|
||||
node .cursor/skills/impeccable/scripts/critique-storage.mjs slug "<resolved-path-or-url>"
|
||||
```
|
||||
Keep the printed slug. It identifies this target's stream across runs. If the command exits non-zero ("no stable slug for input"), skip persistence for this run and tell the user; the trend won't update but the critique still goes ahead.
|
||||
|
||||
3. **Read the ignore list** at `.impeccable/critique/ignore.md` if it exists. Plain markdown; each non-empty, non-comment line is something the user has marked as "do not re-raise" (deferred tradeoffs, designer-intended deviations, detector false-positives the user accepts). When a finding's text matches a line here (case-insensitive substring against rule name or snippet), **drop it silently**. Do not mention it in the report. This is the ONLY input critique consumes from prior runs; anchoring on prior findings would defeat the point of independent assessment.
|
||||
|
||||
### Gather Assessments
|
||||
|
||||
Launch two independent assessments. **Neither may see the other's output.** This isolation is what makes the combined score honest. Running both in one head silently anchors them to each other; do not shortcut it for cost, speed, or context-size reasons.
|
||||
@@ -164,6 +182,36 @@ Provocative questions that might unlock better solutions:
|
||||
- Prioritize ruthlessly. If everything is important, nothing is.
|
||||
- Don't soften criticism. Developers need honest feedback to ship great design.
|
||||
|
||||
### Persist the Snapshot
|
||||
|
||||
Once the report above is finalized, write it to `.impeccable/critique/` so the user can refer back, and so `/impeccable polish` can pick up the priority issues without a copy-paste.
|
||||
|
||||
Skip this step if the Setup slug was null (vague or root-level target).
|
||||
|
||||
1. **Write the body to a temp file** so you can pipe it to the helper. Use the full report (heuristic table, anti-patterns verdict, priority issues, persona red flags) but stop before the "Ask the User" / "Recommended Actions" sections that come later.
|
||||
|
||||
2. **Pass the structured metadata** through `IMPECCABLE_CRITIQUE_META` (JSON), then run the write command:
|
||||
```bash
|
||||
IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"p0_count":<n>,"p1_count":<n>}' \
|
||||
node .cursor/skills/impeccable/scripts/critique-storage.mjs write <slug> <body-file>
|
||||
```
|
||||
The helper prints the absolute path it wrote.
|
||||
|
||||
3. **Read the trend** for context:
|
||||
```bash
|
||||
node .cursor/skills/impeccable/scripts/critique-storage.mjs trend <slug> 5
|
||||
```
|
||||
This returns a JSON array of the last 5 frontmatter entries (including the one you just wrote).
|
||||
|
||||
4. **Append a single line to the user-visible output**, after the report and before the questions:
|
||||
|
||||
> **Trend for `<slug>` (last 5 runs): 24 → 28 → 32 → 29 → 32**
|
||||
> Wrote `.impeccable/critique/<filename>`.
|
||||
|
||||
If this is the first run for the slug, the trend is just one score; say so: "First run for this target, no trend yet."
|
||||
|
||||
This is fire-and-forget. Do not show the user the helper's JSON output; only the human-readable trend line and the written path. Failures here should not block the rest of the flow; print the error and move on.
|
||||
|
||||
### Ask the User
|
||||
|
||||
**After presenting findings**, use targeted questions based on what was actually found. ask the user directly to clarify what you cannot infer. These answers will shape the action plan.
|
||||
|
||||
@@ -35,7 +35,14 @@ Understand the current state and goals before touching anything:
|
||||
- Loading and transition smoothness
|
||||
- Information architecture and flow drift (does this feature reveal complexity the way neighboring features do?)
|
||||
|
||||
4. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent; never perfect one corner while leaving another rough.
|
||||
4. **Pull in any prior critique** (optional signal): If `/impeccable critique` has been run on the same target, its priority issues are a useful prior for what to address first. Resolve the target to a file path or URL, then:
|
||||
```bash
|
||||
slug=$(node .cursor/skills/impeccable/scripts/critique-storage.mjs slug "<resolved>")
|
||||
node .cursor/skills/impeccable/scripts/critique-storage.mjs latest "$slug"
|
||||
```
|
||||
Exit 0 with body = found; fold the P0/P1 items into your polish list and mention the snapshot path so the user sees what you read. Exit 2 = no snapshot, continue without it. The critique is one input among many. Do your own pass either way.
|
||||
|
||||
5. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent; never perfect one corner while leaving another rough.
|
||||
|
||||
**CRITICAL**: Polish is the last step, not the first. Don't polish work that's not functionally complete.
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ After the discovery interview, generate a small set of visual direction probes *
|
||||
- The requested fidelity is **mid-fi, high-fi, or production-ready**. Skip for sketch-only planning.
|
||||
- The current harness gives you native image generation (Codex's `image_gen`, an equivalent MCP tool, or similar). Don't ask the user to install APIs or tooling.
|
||||
|
||||
When those conditions are met, this step is mandatory. If image generation isn't natively available, skip silently and proceed; don't announce the skip.
|
||||
When those conditions are met, this step is mandatory. If image generation isn't natively available, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed. The one-line announcement is required, not optional; it forces a conscious decision instead of letting the step quietly evaporate.
|
||||
|
||||
Use probes to explore visual lanes, not to replace the brief.
|
||||
|
||||
@@ -107,7 +107,7 @@ The probes should differ in primary visual direction (hierarchy, topology, densi
|
||||
- Do **not** treat generated imagery as final UX specification, final copy, or final accessibility behavior.
|
||||
- Do **not** use this step for minor refinements of existing work. It's for shaping a new surface or clarifying a big directional choice.
|
||||
|
||||
If image generation isn't natively available, skip this phase silently and proceed.
|
||||
If image generation isn't natively available, announce the skip in one line and proceed to the design brief.
|
||||
|
||||
## Phase 2: Design Brief
|
||||
|
||||
@@ -120,7 +120,7 @@ After the interview and any required probes, present a brief and **end your resp
|
||||
|
||||
Don't pad a clear brief into a long one to look thorough. A 70-line brief restating answers the user just gave is noise, not rigor. Equally, don't skip the confirmation pause to look efficient: the pause is the point.
|
||||
|
||||
If the user already said "approved" or "go" during discovery for the *exact direction you'd present*, that counts as confirmation; you may proceed without asking again. But if your brief adds anything the user hasn't seen and approved, you must stop and confirm.
|
||||
Present the brief, then **stop and wait for explicit confirmation**. You are not the judge of whether the user already approved. Even when the brief feels obviously right, ask once and wait. The pause is what separates shape from premature implementation.
|
||||
|
||||
### Brief Structure
|
||||
|
||||
@@ -158,8 +158,8 @@ Anything genuinely unresolved. Don't list "open questions" you've already recomm
|
||||
|
||||
---
|
||||
|
||||
ask the user directly to clarify what you cannot infer.
|
||||
ask the user directly to clarify what you cannot infer. Ask for explicit confirmation of the brief before finishing.
|
||||
|
||||
If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the user confirms direction (or the user already gave a clear go during discovery, which counts).
|
||||
If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the user confirms direction.
|
||||
|
||||
Once confirmed, the brief is complete. The user can now hand it to /impeccable, or use it to guide any other implementation approach. (If the user wants the full discovery-then-build flow in one step, they should use /impeccable craft instead, which runs this command internally.)
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Critique persistence helper.
|
||||
*
|
||||
* Each run of /impeccable critique writes a per-target snapshot to
|
||||
* .impeccable/critique/<timestamp>__<slug>.md
|
||||
* with a small YAML frontmatter carrying the score + P0/P1 counts.
|
||||
*
|
||||
* /impeccable polish reads the latest matching snapshot at start as its
|
||||
* fix backlog. No other skill auto-reads critique output.
|
||||
*
|
||||
* The slug is derived mechanically from the *resolved* primary artifact
|
||||
* (file path or URL), never from the user's natural-language phrasing.
|
||||
* Slug stability across runs is what lets the trend display work.
|
||||
*
|
||||
* CLI entry points (called from skill instructions):
|
||||
* node critique-storage.mjs slug <resolved-target>
|
||||
* node critique-storage.mjs write <slug> <snapshot-body-file>
|
||||
* node critique-storage.mjs latest <slug>
|
||||
* node critique-storage.mjs trend <slug> [limit]
|
||||
*
|
||||
* Note: there is intentionally no `ignore` subcommand. ignore.md is a plain
|
||||
* markdown file; the model reads it directly with its file-read tool. This
|
||||
* helper only exists for operations the model can't trivially do inline
|
||||
* (normalizing paths, generating filenames, globbing + parsing frontmatter).
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { getCritiqueDir } from './impeccable-paths.mjs';
|
||||
|
||||
const SLUG_MAX = 50;
|
||||
|
||||
/**
|
||||
* Mechanically derive a slug from a resolved target. Returns null if the
|
||||
* input doesn't look like a stable identifier (empty, project root, etc).
|
||||
*
|
||||
* Accepts file paths and URLs. The model resolves "the homepage" to a
|
||||
* concrete artifact before calling this — we never slug a natural-language
|
||||
* phrase.
|
||||
*/
|
||||
export function slugFromTarget(resolved, { cwd = process.cwd() } = {}) {
|
||||
if (!resolved || typeof resolved !== 'string') return null;
|
||||
const trimmed = resolved.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
// URL
|
||||
if (/^https?:\/\//i.test(trimmed)) {
|
||||
let url;
|
||||
try { url = new URL(trimmed); } catch { return null; }
|
||||
const hostPath = `${url.hostname}${url.pathname}`;
|
||||
return kebab(hostPath);
|
||||
}
|
||||
|
||||
// File path. Make it project-relative so two devs critiquing the same
|
||||
// checkout get the same slug regardless of where their repo is cloned.
|
||||
const abs = path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
|
||||
let rel = path.relative(cwd, abs);
|
||||
// If the target is outside cwd, fall back to the basename so we still
|
||||
// produce a stable slug (vs the absolute path, which would include
|
||||
// home dirs / usernames).
|
||||
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
||||
rel = path.basename(abs);
|
||||
}
|
||||
if (!rel || rel === '.' || rel === '') return null;
|
||||
return kebab(rel);
|
||||
}
|
||||
|
||||
function kebab(s) {
|
||||
const slug = s
|
||||
.toLowerCase()
|
||||
.replace(/[/\\.]+/g, '-')
|
||||
.replace(/[^a-z0-9-]+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
if (!slug) return null;
|
||||
// Cap from the tail — the tail (filename) is more identifying than the
|
||||
// top-level directory.
|
||||
return slug.length <= SLUG_MAX ? slug : slug.slice(slug.length - SLUG_MAX).replace(/^-/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filename-safe UTC ISO timestamp: hyphens for separators, trailing Z.
|
||||
* Plain colons aren't allowed on Windows filesystems.
|
||||
*/
|
||||
export function nowFilenameStamp(date = new Date()) {
|
||||
const iso = date.toISOString(); // 2026-05-12T18:30:00.123Z
|
||||
return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z');
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a snapshot for `slug`. `meta` carries the small structured frontmatter
|
||||
* keys read back by readTrend(). `body` is the human-readable critique
|
||||
* report (everything below the frontmatter).
|
||||
*
|
||||
* Returns the absolute path written.
|
||||
*/
|
||||
export function writeSnapshot({ slug, meta, body, cwd = process.cwd(), now = new Date() }) {
|
||||
if (!slug) throw new Error('writeSnapshot requires a slug');
|
||||
const dir = getCritiqueDir(cwd);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const timestamp = nowFilenameStamp(now);
|
||||
const filePath = path.join(dir, `${timestamp}__${slug}.md`);
|
||||
// Spread `meta` first so internally computed `timestamp` and `slug`
|
||||
// always win. Otherwise a caller-supplied meta blob (parsed from the
|
||||
// IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the
|
||||
// filename in disagreement with its frontmatter and corrupting trends.
|
||||
const front = serializeFrontmatter({ ...meta, timestamp, slug });
|
||||
fs.writeFileSync(filePath, `${front}\n${body.trim()}\n`, 'utf-8');
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function serializeFrontmatter(obj) {
|
||||
const lines = ['---'];
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
const str = typeof value === 'string' ? value : String(value);
|
||||
// Quote strings that contain : or # to keep parsing simple.
|
||||
const needsQuotes = typeof value === 'string' && /[:#]/.test(str);
|
||||
lines.push(`${key}: ${needsQuotes ? JSON.stringify(str) : str}`);
|
||||
}
|
||||
lines.push('---');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function parseFrontmatter(text) {
|
||||
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
if (!match) return {};
|
||||
const out = {};
|
||||
for (const line of match[1].split(/\r?\n/)) {
|
||||
const colon = line.indexOf(':');
|
||||
if (colon < 0) continue;
|
||||
const key = line.slice(0, colon).trim();
|
||||
let value = line.slice(colon + 1).trim();
|
||||
if (/^".*"$/.test(value)) {
|
||||
try { value = JSON.parse(value); } catch { /* leave as-is */ }
|
||||
} else if (/^-?\d+$/.test(value)) {
|
||||
value = Number(value);
|
||||
}
|
||||
out[key] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all snapshot files for `slug`, sorted oldest → newest.
|
||||
*/
|
||||
function listSnapshotsForSlug(slug, cwd) {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const suffix = `__${slug}.md`;
|
||||
return fs.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(suffix))
|
||||
.sort()
|
||||
.map((f) => path.join(dir, f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent snapshot for `slug`, or null. Polish reads this
|
||||
* to find its fix backlog when the slug matches.
|
||||
*/
|
||||
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
if (!all.length) return null;
|
||||
const latest = all[all.length - 1];
|
||||
const body = fs.readFileSync(latest, 'utf-8');
|
||||
return { path: latest, body, meta: parseFrontmatter(body) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last `limit` snapshots' frontmatter, oldest → newest.
|
||||
* Critique appends a one-line trend to its output using this.
|
||||
*/
|
||||
export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
const slice = all.slice(-limit);
|
||||
return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8')));
|
||||
}
|
||||
|
||||
// ---- CLI ---------------------------------------------------------------
|
||||
|
||||
function main(argv) {
|
||||
const [cmd, ...args] = argv;
|
||||
switch (cmd) {
|
||||
case 'slug': {
|
||||
const slug = slugFromTarget(args[0]);
|
||||
if (!slug) { process.stderr.write('no stable slug for input\n'); process.exit(1); }
|
||||
process.stdout.write(`${slug}\n`);
|
||||
return;
|
||||
}
|
||||
case 'write': {
|
||||
const [slug, bodyFile] = args;
|
||||
if (!slug || !bodyFile) { process.stderr.write('usage: write <slug> <body-file>\n'); process.exit(1); }
|
||||
const raw = fs.readFileSync(bodyFile, 'utf-8');
|
||||
// The body file may be a full report. The caller passes the meta as
|
||||
// a JSON object on stdin if it wants structured frontmatter; otherwise
|
||||
// we write with minimal metadata.
|
||||
let meta = {};
|
||||
const metaArg = process.env.IMPECCABLE_CRITIQUE_META;
|
||||
if (metaArg) {
|
||||
try { meta = JSON.parse(metaArg); } catch { /* ignore */ }
|
||||
}
|
||||
const out = writeSnapshot({ slug, meta, body: raw });
|
||||
process.stdout.write(`${out}\n`);
|
||||
return;
|
||||
}
|
||||
case 'latest': {
|
||||
const latest = readLatestSnapshot(args[0]);
|
||||
if (!latest) { process.exit(2); }
|
||||
process.stdout.write(latest.body);
|
||||
return;
|
||||
}
|
||||
case 'trend': {
|
||||
const rows = readTrend(args[0], { limit: args[1] ? Number(args[1]) : 5 });
|
||||
process.stdout.write(JSON.stringify(rows, null, 2) + '\n');
|
||||
return;
|
||||
}
|
||||
default:
|
||||
process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend> [args]\n');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main(process.argv.slice(2));
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import path from 'node:path';
|
||||
|
||||
export const IMPECCABLE_DIR = '.impeccable';
|
||||
export const LIVE_DIR = 'live';
|
||||
export const CRITIQUE_DIR = 'critique';
|
||||
|
||||
export function getImpeccableDir(cwd = process.cwd()) {
|
||||
return path.join(cwd, IMPECCABLE_DIR);
|
||||
@@ -96,6 +97,10 @@ export function getLiveAnnotationsDir(cwd = process.cwd()) {
|
||||
return path.join(getLiveDir(cwd), 'annotations');
|
||||
}
|
||||
|
||||
export function getCritiqueDir(cwd = process.cwd()) {
|
||||
return path.join(getImpeccableDir(cwd), CRITIQUE_DIR);
|
||||
}
|
||||
|
||||
export function getLegacyLiveAnnotationsDir(cwd = process.cwd()) {
|
||||
return path.join(cwd, '.impeccable-live', 'annotations');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 3.0.7
|
||||
version: 3.1.0
|
||||
---
|
||||
|
||||
Designs and iterates production-grade frontend interfaces. Real working code, committed design choices, exceptional craft.
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# Codex: Visual Direction & Asset Production
|
||||
|
||||
This file is loaded by `/impeccable craft` when the harness has native image generation (currently Codex via `image_gen`). Other harnesses skip it. It covers the two craft steps that depend on real image generation: landing the visual direction, and producing the raster assets the implementation will compose.
|
||||
|
||||
Read this *before* generating any images. The order matters, and the per-step user pauses are what keep generated imagery from drifting away from the brief.
|
||||
|
||||
## Step A: Explore Directions with the User
|
||||
|
||||
Before generating anything, run a brief direction conversation grounded in the shape brief.
|
||||
|
||||
Ask **2-3 targeted questions** about visual lane, color strategy, atmosphere, and named anchor references. Don't enumerate generic menus; tie each question to the shape brief's answers. Example shape-grounded questions:
|
||||
|
||||
- "Brief says 'editorial restraint, Klim-adjacent.' Are we closer to a quiet specimen page or a magazine-spread feel with hero imagery?"
|
||||
- "Palette strategy from shape was 'Committed.' Want it warm-grounded (deep oxblood + cream) or cool-grounded (slate + paper white)?"
|
||||
|
||||
Stop and wait for answers. These pin the palette before any pixel gets generated.
|
||||
|
||||
## Step B: Generate the Brand Palette First
|
||||
|
||||
Generate **one** palette artifact before any mocks. This is a small, focused image: typography pairing on the chosen background, primary + accent color swatches, one signature ornament or motif. Single image, single pass.
|
||||
|
||||
Why palette first: mocks generated against a vague color sense produce noise that drowns out the structural decisions. A confirmed palette is the first concrete contract for everything downstream.
|
||||
|
||||
Show the palette to the user. Ask one question: "This is the palette I'm locking in for the mocks. Confirm, or call out what to shift?"
|
||||
|
||||
Wait for confirmation. Do not generate mocks against an unconfirmed palette.
|
||||
|
||||
## Step C: Generate 1-3 Visual Mocks Against the Palette
|
||||
|
||||
Once the palette is confirmed, generate **1 to 3** high-fidelity north-star comps. Each mock must use the confirmed palette and typography. Mocks differ in *structural* direction (hierarchy, topology, density, composition), not in color or motif.
|
||||
|
||||
- Brand work: push visual identity, composition, mood, and signature motifs.
|
||||
- Product work: push hierarchy, topology, density, tone, grounded in realistic product structure.
|
||||
- Landing pages and long-form brand surfaces: show enough of the second fold to establish the system beyond the hero.
|
||||
|
||||
Use the `image_gen` tool directly (or via the imagegen skill when available). Don't ask the user to install anything.
|
||||
|
||||
## Step D: Approval Loop
|
||||
|
||||
Show the comps. Ask what carries forward. Iterate until **one direction is approved** or the user explicitly delegates.
|
||||
|
||||
If the user delegates, pick the strongest direction and explain it from the brief, not personal taste.
|
||||
|
||||
Before moving to assets, summarize what to carry into code and what *not* to literalize from the mock. This is the handoff between visual exploration and semantic implementation.
|
||||
|
||||
## Step E: Mock Fidelity Inventory
|
||||
|
||||
Inventory the approved mock's major visible ingredients. For each, decide implementation: semantic HTML/CSS/SVG, generated raster, sourced raster, icon library, canvas/WebGL, or accepted omission.
|
||||
|
||||
Common ingredients to inventory:
|
||||
|
||||
- Hero silhouette and dominant composition
|
||||
- Signature motifs (planets, devices, portraits, charts, route lines, insets, badges, etc.)
|
||||
- Nav and primary CTA treatment
|
||||
- Section sequence, especially the second fold
|
||||
- Image-native content the concept depends on
|
||||
- Typography, density, color/material treatment, motion cues
|
||||
|
||||
Treat the mock as a north star, not a screenshot to trace. Don't rasterize core UI text. But if the live result lacks the mock's major ingredients, the implementation is wrong.
|
||||
|
||||
If a photographic, architectural, product, or place-led mock becomes generic CSS scenery, decorative diagrams, bullets, or copy, stop and fix it. That's a broken implementation, not a harmless interpretation.
|
||||
|
||||
Don't substitute a different hero composition or visual driver post-approval without user sign-off.
|
||||
|
||||
## Step F: Asset Slicing via the Asset Producer
|
||||
|
||||
Raster ingredients identified in Step E need clean production assets. Use the bundled `impeccable_asset_producer` subagent rather than producing inline.
|
||||
|
||||
Spawn it as a scoped subagent. If you do not have explicit permission to use agents, stop and ask:
|
||||
|
||||
```text
|
||||
Asset production will work better as a scoped subagent job. Should I spawn the Impeccable asset producer subagent for this step?
|
||||
```
|
||||
|
||||
Pass to the agent:
|
||||
|
||||
- Approved mock path or screenshot reference
|
||||
- Crop paths or a contact sheet with crop ids
|
||||
- Output directory
|
||||
- Required dimensions, format, transparency needs
|
||||
- Avoid list
|
||||
- Notes on what should remain semantic HTML/CSS/SVG instead of raster
|
||||
|
||||
Attach image generation capability to the spawned agent when the harness supports it. Do **not** load image-generation reference material into the parent thread.
|
||||
|
||||
Inline asset production is allowed only if the user declines subagents, the harness cannot spawn the authorized agent, or the user explicitly asks for single-thread mode.
|
||||
|
||||
Prefer HTML/CSS/SVG/canvas when they can credibly reproduce an ingredient; reach for real, generated, or stock imagery when the mock or subject matter calls for actual visual content.
|
||||
|
||||
## After This File
|
||||
|
||||
Once Steps A through F are complete, return to `craft.md` Step 5 (Build to Production Quality). The implementation builds against the confirmed palette, approved mock, and the assets the producer wrote.
|
||||
@@ -51,64 +51,15 @@ Then add references based on the brief's needs:
|
||||
- Responsive requirements? Consult [responsive-design.md](responsive-design.md)
|
||||
- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.md)
|
||||
|
||||
## Step 3: Land the Visual Direction (Capability-Gated)
|
||||
## Step 3: Visual Direction & Assets (Harness-Gated)
|
||||
|
||||
Generate high-fidelity visual comps before implementation when all three are true:
|
||||
If the harness has **native image generation** (currently Codex via `image_gen`), this step is mandatory. **Stop and load [codex.md](codex.md)**. It covers palette generation, mock exploration, the approval loop, mock-fidelity inventory, and asset slicing via the `impeccable_asset_producer` subagent. Follow Steps A-F in that file, then return here for Step 4.
|
||||
|
||||
- The work is net-new or visually open-ended enough that composition exploration will improve the build.
|
||||
- The brief's scope is mid-fi, high-fi, or production-ready.
|
||||
- The harness gives you native image generation (Codex's `image_gen`, an equivalent MCP tool). Don't ask the user to set up external APIs.
|
||||
If the harness lacks native image generation, **state in one line that the visual-direction-by-generation step is being skipped because the harness lacks native image generation, then proceed**. The one-line announcement is required; it forces a conscious decision instead of letting the step quietly evaporate. The brief is your only visual reference. Implement directly from it, treating any named anchor references and the brief's "Design Direction" as the contract.
|
||||
|
||||
When met, this step is mandatory for both brand and product work. If image generation isn't available, skip silently. "The eventual UI is semantic/code-native/accessible" is not a reason to skip; those are implementation requirements, not exploration ones.
|
||||
Whether you generated mocks or not: don't replace required imagery with generic cards, bullets, emoji, fake metrics, decorative CSS panels, or filler copy. Image-led briefs (restaurants, hotels, magazines, photography, hobbyist communities, food, travel, fashion, product) need real or sourced imagery in the build, not CSS scenery.
|
||||
|
||||
### What to generate
|
||||
|
||||
Generate **1 to 3** high-fidelity north-star comps from the confirmed brief. If shape already produced direction probes, resolve the winning lane further, not an unrelated exploration. Comps must differ in primary visual direction, not just color.
|
||||
|
||||
- Brand work: push visual identity, composition, and mood aggressively.
|
||||
- Product work: push hierarchy, topology, density, tone, grounded in realistic product structure.
|
||||
- Landing pages and long-form brand surfaces: show enough of the second fold to establish the system beyond the hero.
|
||||
|
||||
### Approval loop
|
||||
|
||||
Show the comps and ask what carries forward. Iterate until one direction is approved or the user delegates. If the user delegates, pick the strongest direction and explain it from the brief, not personal taste.
|
||||
|
||||
Before Step 4, summarize what to carry into code and what **not** to literalize from the mock. This is the handoff between visual exploration and semantic implementation.
|
||||
|
||||
### Mock fidelity inventory
|
||||
|
||||
Inventory the approved mock's major visible ingredients:
|
||||
|
||||
- Hero silhouette and dominant composition
|
||||
- Signature motifs (planets, devices, portraits, charts, route lines, insets, badges, etc.)
|
||||
- Nav and primary CTA treatment
|
||||
- Section sequence, especially the second fold
|
||||
- Image-native content the concept depends on
|
||||
- Typography, density, color/material treatment, motion cues
|
||||
|
||||
For each, decide implementation: semantic HTML/CSS/SVG, generated asset, sourced asset, icon library, canvas/WebGL, or accepted omission. Don't substitute a different hero composition or visual driver post-approval without user sign-off.
|
||||
|
||||
If a photographic, architectural, product, or place-led mock becomes generic CSS scenery / decorative diagrams / bullets / copy, stop and fix it. That's a broken implementation, not a harmless interpretation.
|
||||
|
||||
Treat the mock as a north star, not a screenshot to trace. Don't rasterize core UI text. But if the live result lacks the mock's major ingredients, the implementation is wrong.
|
||||
|
||||
## Step 4: Asset Extraction (Need-Gated)
|
||||
|
||||
If the approved direction needs raster assets, create them before building. Do not replace required imagery with generic cards, bullets, emoji, fake metrics, decorative CSS panels, or filler copy.
|
||||
|
||||
Use the native asset producer (`impeccable_asset_producer` in Codex, `impeccable-asset-producer` in Claude Code) to create clean assets from the hi-fi mock and crops. If you do not have explicit permission to use agents, stop and ask:
|
||||
|
||||
```text
|
||||
Asset production will work better as a scoped subagent job. Should I spawn the Impeccable asset producer subagent for this step?
|
||||
```
|
||||
|
||||
Do not skip asset production or silently do it inline. Inline asset production is allowed only if the user declines subagents, the harness cannot spawn the authorized agent, or the user explicitly asks for single-thread mode.
|
||||
|
||||
Pass the approved mock, crop/contact-sheet paths, output directory, dimensions/formats, transparency needs, constraints, and avoid list to the asset producer. Attach image generation capability to the spawned agent when the harness supports it; do not load image-generation reference material into the parent thread first.
|
||||
|
||||
Prefer HTML/CSS/SVG/canvas when they can credibly reproduce an ingredient; use real/generated/stock imagery when the mock or subject matter calls for actual visual content.
|
||||
|
||||
## Step 5: Build to Production Quality
|
||||
## Step 4: Build to Production Quality
|
||||
|
||||
Implement the feature following the design brief. Build in passes so structure, visual system, states, motion/media, and responsive behavior each get deliberate attention. The list below is the definition of done, not inspiration.
|
||||
|
||||
@@ -130,7 +81,7 @@ Implement the feature following the design brief. Build in passes so structure,
|
||||
- **Technically clean.** Production build passes, no console errors, no avoidable layout shift, no needless dependencies, no broken asset paths.
|
||||
- **Ask when uncertain.** If a discovery materially changes the brief or approved direction, stop and ask. Don't guess.
|
||||
|
||||
## Step 6: Iterate Visually
|
||||
## Step 5: Iterate Visually
|
||||
|
||||
Look at what you built like a designer would. Your eyes are whatever the harness gives you: a connected browser, a screenshotting tool, Playwright, or asking the user. Use them for responsive testing (mobile, tablet, desktop minimum) and general visual validation.
|
||||
|
||||
@@ -144,7 +95,7 @@ Actively check: responsive behavior (composes, not shrinks), every state (empty
|
||||
|
||||
Detector or QA output is defect evidence only; never proof the work is finished.
|
||||
|
||||
## Step 7: Present
|
||||
## Step 6: Present
|
||||
|
||||
Present the result to the user:
|
||||
- Show the feature in its primary state
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
> **Additional context needed**: what the interface is trying to accomplish.
|
||||
|
||||
### Setup: Resolve Target and Load Ignore List
|
||||
|
||||
Before gathering assessments, do two small bookkeeping steps. They cost almost nothing and they're what makes critique iterative across runs.
|
||||
|
||||
1. **Resolve the primary artifact.** The user's phrasing ("the homepage", "the pricing flow") is not stable enough to track across runs. Resolve it to a concrete file path or URL: the same one you'd already need to scan code or open in a browser. Examples:
|
||||
- "the homepage" → `site/pages/index.astro` (or `http://localhost:3000/` if you're inspecting live)
|
||||
- "the settings modal" → the primary component file (e.g., `src/components/Settings.tsx`)
|
||||
- "this page" → the URL or the page's source file
|
||||
Prefer the source file path over the dev-server URL when both exist; ports drift between runs (`bun dev` vs `bun preview`), file paths don't.
|
||||
|
||||
2. **Compute the slug.** Run:
|
||||
```bash
|
||||
node .gemini/skills/impeccable/scripts/critique-storage.mjs slug "<resolved-path-or-url>"
|
||||
```
|
||||
Keep the printed slug. It identifies this target's stream across runs. If the command exits non-zero ("no stable slug for input"), skip persistence for this run and tell the user; the trend won't update but the critique still goes ahead.
|
||||
|
||||
3. **Read the ignore list** at `.impeccable/critique/ignore.md` if it exists. Plain markdown; each non-empty, non-comment line is something the user has marked as "do not re-raise" (deferred tradeoffs, designer-intended deviations, detector false-positives the user accepts). When a finding's text matches a line here (case-insensitive substring against rule name or snippet), **drop it silently**. Do not mention it in the report. This is the ONLY input critique consumes from prior runs; anchoring on prior findings would defeat the point of independent assessment.
|
||||
|
||||
### Gather Assessments
|
||||
|
||||
Launch two independent assessments. **Neither may see the other's output.** This isolation is what makes the combined score honest. Running both in one head silently anchors them to each other; do not shortcut it for cost, speed, or context-size reasons.
|
||||
@@ -164,6 +182,36 @@ Provocative questions that might unlock better solutions:
|
||||
- Prioritize ruthlessly. If everything is important, nothing is.
|
||||
- Don't soften criticism. Developers need honest feedback to ship great design.
|
||||
|
||||
### Persist the Snapshot
|
||||
|
||||
Once the report above is finalized, write it to `.impeccable/critique/` so the user can refer back, and so `/impeccable polish` can pick up the priority issues without a copy-paste.
|
||||
|
||||
Skip this step if the Setup slug was null (vague or root-level target).
|
||||
|
||||
1. **Write the body to a temp file** so you can pipe it to the helper. Use the full report (heuristic table, anti-patterns verdict, priority issues, persona red flags) but stop before the "Ask the User" / "Recommended Actions" sections that come later.
|
||||
|
||||
2. **Pass the structured metadata** through `IMPECCABLE_CRITIQUE_META` (JSON), then run the write command:
|
||||
```bash
|
||||
IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"p0_count":<n>,"p1_count":<n>}' \
|
||||
node .gemini/skills/impeccable/scripts/critique-storage.mjs write <slug> <body-file>
|
||||
```
|
||||
The helper prints the absolute path it wrote.
|
||||
|
||||
3. **Read the trend** for context:
|
||||
```bash
|
||||
node .gemini/skills/impeccable/scripts/critique-storage.mjs trend <slug> 5
|
||||
```
|
||||
This returns a JSON array of the last 5 frontmatter entries (including the one you just wrote).
|
||||
|
||||
4. **Append a single line to the user-visible output**, after the report and before the questions:
|
||||
|
||||
> **Trend for `<slug>` (last 5 runs): 24 → 28 → 32 → 29 → 32**
|
||||
> Wrote `.impeccable/critique/<filename>`.
|
||||
|
||||
If this is the first run for the slug, the trend is just one score; say so: "First run for this target, no trend yet."
|
||||
|
||||
This is fire-and-forget. Do not show the user the helper's JSON output; only the human-readable trend line and the written path. Failures here should not block the rest of the flow; print the error and move on.
|
||||
|
||||
### Ask the User
|
||||
|
||||
**After presenting findings**, use targeted questions based on what was actually found. ask the user directly to clarify what you cannot infer. These answers will shape the action plan.
|
||||
|
||||
@@ -35,7 +35,14 @@ Understand the current state and goals before touching anything:
|
||||
- Loading and transition smoothness
|
||||
- Information architecture and flow drift (does this feature reveal complexity the way neighboring features do?)
|
||||
|
||||
4. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent; never perfect one corner while leaving another rough.
|
||||
4. **Pull in any prior critique** (optional signal): If `/impeccable critique` has been run on the same target, its priority issues are a useful prior for what to address first. Resolve the target to a file path or URL, then:
|
||||
```bash
|
||||
slug=$(node .gemini/skills/impeccable/scripts/critique-storage.mjs slug "<resolved>")
|
||||
node .gemini/skills/impeccable/scripts/critique-storage.mjs latest "$slug"
|
||||
```
|
||||
Exit 0 with body = found; fold the P0/P1 items into your polish list and mention the snapshot path so the user sees what you read. Exit 2 = no snapshot, continue without it. The critique is one input among many. Do your own pass either way.
|
||||
|
||||
5. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent; never perfect one corner while leaving another rough.
|
||||
|
||||
**CRITICAL**: Polish is the last step, not the first. Don't polish work that's not functionally complete.
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ After the discovery interview, generate a small set of visual direction probes *
|
||||
- The requested fidelity is **mid-fi, high-fi, or production-ready**. Skip for sketch-only planning.
|
||||
- The current harness gives you native image generation (Codex's `image_gen`, an equivalent MCP tool, or similar). Don't ask the user to install APIs or tooling.
|
||||
|
||||
When those conditions are met, this step is mandatory. If image generation isn't natively available, skip silently and proceed; don't announce the skip.
|
||||
When those conditions are met, this step is mandatory. If image generation isn't natively available, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed. The one-line announcement is required, not optional; it forces a conscious decision instead of letting the step quietly evaporate.
|
||||
|
||||
Use probes to explore visual lanes, not to replace the brief.
|
||||
|
||||
@@ -107,7 +107,7 @@ The probes should differ in primary visual direction (hierarchy, topology, densi
|
||||
- Do **not** treat generated imagery as final UX specification, final copy, or final accessibility behavior.
|
||||
- Do **not** use this step for minor refinements of existing work. It's for shaping a new surface or clarifying a big directional choice.
|
||||
|
||||
If image generation isn't natively available, skip this phase silently and proceed.
|
||||
If image generation isn't natively available, announce the skip in one line and proceed to the design brief.
|
||||
|
||||
## Phase 2: Design Brief
|
||||
|
||||
@@ -120,7 +120,7 @@ After the interview and any required probes, present a brief and **end your resp
|
||||
|
||||
Don't pad a clear brief into a long one to look thorough. A 70-line brief restating answers the user just gave is noise, not rigor. Equally, don't skip the confirmation pause to look efficient: the pause is the point.
|
||||
|
||||
If the user already said "approved" or "go" during discovery for the *exact direction you'd present*, that counts as confirmation; you may proceed without asking again. But if your brief adds anything the user hasn't seen and approved, you must stop and confirm.
|
||||
Present the brief, then **stop and wait for explicit confirmation**. You are not the judge of whether the user already approved. Even when the brief feels obviously right, ask once and wait. The pause is what separates shape from premature implementation.
|
||||
|
||||
### Brief Structure
|
||||
|
||||
@@ -158,8 +158,8 @@ Anything genuinely unresolved. Don't list "open questions" you've already recomm
|
||||
|
||||
---
|
||||
|
||||
ask the user directly to clarify what you cannot infer.
|
||||
ask the user directly to clarify what you cannot infer. Ask for explicit confirmation of the brief before finishing.
|
||||
|
||||
If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the user confirms direction (or the user already gave a clear go during discovery, which counts).
|
||||
If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the user confirms direction.
|
||||
|
||||
Once confirmed, the brief is complete. The user can now hand it to /impeccable, or use it to guide any other implementation approach. (If the user wants the full discovery-then-build flow in one step, they should use /impeccable craft instead, which runs this command internally.)
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Critique persistence helper.
|
||||
*
|
||||
* Each run of /impeccable critique writes a per-target snapshot to
|
||||
* .impeccable/critique/<timestamp>__<slug>.md
|
||||
* with a small YAML frontmatter carrying the score + P0/P1 counts.
|
||||
*
|
||||
* /impeccable polish reads the latest matching snapshot at start as its
|
||||
* fix backlog. No other skill auto-reads critique output.
|
||||
*
|
||||
* The slug is derived mechanically from the *resolved* primary artifact
|
||||
* (file path or URL), never from the user's natural-language phrasing.
|
||||
* Slug stability across runs is what lets the trend display work.
|
||||
*
|
||||
* CLI entry points (called from skill instructions):
|
||||
* node critique-storage.mjs slug <resolved-target>
|
||||
* node critique-storage.mjs write <slug> <snapshot-body-file>
|
||||
* node critique-storage.mjs latest <slug>
|
||||
* node critique-storage.mjs trend <slug> [limit]
|
||||
*
|
||||
* Note: there is intentionally no `ignore` subcommand. ignore.md is a plain
|
||||
* markdown file; the model reads it directly with its file-read tool. This
|
||||
* helper only exists for operations the model can't trivially do inline
|
||||
* (normalizing paths, generating filenames, globbing + parsing frontmatter).
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { getCritiqueDir } from './impeccable-paths.mjs';
|
||||
|
||||
const SLUG_MAX = 50;
|
||||
|
||||
/**
|
||||
* Mechanically derive a slug from a resolved target. Returns null if the
|
||||
* input doesn't look like a stable identifier (empty, project root, etc).
|
||||
*
|
||||
* Accepts file paths and URLs. The model resolves "the homepage" to a
|
||||
* concrete artifact before calling this — we never slug a natural-language
|
||||
* phrase.
|
||||
*/
|
||||
export function slugFromTarget(resolved, { cwd = process.cwd() } = {}) {
|
||||
if (!resolved || typeof resolved !== 'string') return null;
|
||||
const trimmed = resolved.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
// URL
|
||||
if (/^https?:\/\//i.test(trimmed)) {
|
||||
let url;
|
||||
try { url = new URL(trimmed); } catch { return null; }
|
||||
const hostPath = `${url.hostname}${url.pathname}`;
|
||||
return kebab(hostPath);
|
||||
}
|
||||
|
||||
// File path. Make it project-relative so two devs critiquing the same
|
||||
// checkout get the same slug regardless of where their repo is cloned.
|
||||
const abs = path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
|
||||
let rel = path.relative(cwd, abs);
|
||||
// If the target is outside cwd, fall back to the basename so we still
|
||||
// produce a stable slug (vs the absolute path, which would include
|
||||
// home dirs / usernames).
|
||||
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
||||
rel = path.basename(abs);
|
||||
}
|
||||
if (!rel || rel === '.' || rel === '') return null;
|
||||
return kebab(rel);
|
||||
}
|
||||
|
||||
function kebab(s) {
|
||||
const slug = s
|
||||
.toLowerCase()
|
||||
.replace(/[/\\.]+/g, '-')
|
||||
.replace(/[^a-z0-9-]+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
if (!slug) return null;
|
||||
// Cap from the tail — the tail (filename) is more identifying than the
|
||||
// top-level directory.
|
||||
return slug.length <= SLUG_MAX ? slug : slug.slice(slug.length - SLUG_MAX).replace(/^-/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filename-safe UTC ISO timestamp: hyphens for separators, trailing Z.
|
||||
* Plain colons aren't allowed on Windows filesystems.
|
||||
*/
|
||||
export function nowFilenameStamp(date = new Date()) {
|
||||
const iso = date.toISOString(); // 2026-05-12T18:30:00.123Z
|
||||
return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z');
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a snapshot for `slug`. `meta` carries the small structured frontmatter
|
||||
* keys read back by readTrend(). `body` is the human-readable critique
|
||||
* report (everything below the frontmatter).
|
||||
*
|
||||
* Returns the absolute path written.
|
||||
*/
|
||||
export function writeSnapshot({ slug, meta, body, cwd = process.cwd(), now = new Date() }) {
|
||||
if (!slug) throw new Error('writeSnapshot requires a slug');
|
||||
const dir = getCritiqueDir(cwd);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const timestamp = nowFilenameStamp(now);
|
||||
const filePath = path.join(dir, `${timestamp}__${slug}.md`);
|
||||
// Spread `meta` first so internally computed `timestamp` and `slug`
|
||||
// always win. Otherwise a caller-supplied meta blob (parsed from the
|
||||
// IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the
|
||||
// filename in disagreement with its frontmatter and corrupting trends.
|
||||
const front = serializeFrontmatter({ ...meta, timestamp, slug });
|
||||
fs.writeFileSync(filePath, `${front}\n${body.trim()}\n`, 'utf-8');
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function serializeFrontmatter(obj) {
|
||||
const lines = ['---'];
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
const str = typeof value === 'string' ? value : String(value);
|
||||
// Quote strings that contain : or # to keep parsing simple.
|
||||
const needsQuotes = typeof value === 'string' && /[:#]/.test(str);
|
||||
lines.push(`${key}: ${needsQuotes ? JSON.stringify(str) : str}`);
|
||||
}
|
||||
lines.push('---');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function parseFrontmatter(text) {
|
||||
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
if (!match) return {};
|
||||
const out = {};
|
||||
for (const line of match[1].split(/\r?\n/)) {
|
||||
const colon = line.indexOf(':');
|
||||
if (colon < 0) continue;
|
||||
const key = line.slice(0, colon).trim();
|
||||
let value = line.slice(colon + 1).trim();
|
||||
if (/^".*"$/.test(value)) {
|
||||
try { value = JSON.parse(value); } catch { /* leave as-is */ }
|
||||
} else if (/^-?\d+$/.test(value)) {
|
||||
value = Number(value);
|
||||
}
|
||||
out[key] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all snapshot files for `slug`, sorted oldest → newest.
|
||||
*/
|
||||
function listSnapshotsForSlug(slug, cwd) {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const suffix = `__${slug}.md`;
|
||||
return fs.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(suffix))
|
||||
.sort()
|
||||
.map((f) => path.join(dir, f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent snapshot for `slug`, or null. Polish reads this
|
||||
* to find its fix backlog when the slug matches.
|
||||
*/
|
||||
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
if (!all.length) return null;
|
||||
const latest = all[all.length - 1];
|
||||
const body = fs.readFileSync(latest, 'utf-8');
|
||||
return { path: latest, body, meta: parseFrontmatter(body) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last `limit` snapshots' frontmatter, oldest → newest.
|
||||
* Critique appends a one-line trend to its output using this.
|
||||
*/
|
||||
export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
const slice = all.slice(-limit);
|
||||
return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8')));
|
||||
}
|
||||
|
||||
// ---- CLI ---------------------------------------------------------------
|
||||
|
||||
function main(argv) {
|
||||
const [cmd, ...args] = argv;
|
||||
switch (cmd) {
|
||||
case 'slug': {
|
||||
const slug = slugFromTarget(args[0]);
|
||||
if (!slug) { process.stderr.write('no stable slug for input\n'); process.exit(1); }
|
||||
process.stdout.write(`${slug}\n`);
|
||||
return;
|
||||
}
|
||||
case 'write': {
|
||||
const [slug, bodyFile] = args;
|
||||
if (!slug || !bodyFile) { process.stderr.write('usage: write <slug> <body-file>\n'); process.exit(1); }
|
||||
const raw = fs.readFileSync(bodyFile, 'utf-8');
|
||||
// The body file may be a full report. The caller passes the meta as
|
||||
// a JSON object on stdin if it wants structured frontmatter; otherwise
|
||||
// we write with minimal metadata.
|
||||
let meta = {};
|
||||
const metaArg = process.env.IMPECCABLE_CRITIQUE_META;
|
||||
if (metaArg) {
|
||||
try { meta = JSON.parse(metaArg); } catch { /* ignore */ }
|
||||
}
|
||||
const out = writeSnapshot({ slug, meta, body: raw });
|
||||
process.stdout.write(`${out}\n`);
|
||||
return;
|
||||
}
|
||||
case 'latest': {
|
||||
const latest = readLatestSnapshot(args[0]);
|
||||
if (!latest) { process.exit(2); }
|
||||
process.stdout.write(latest.body);
|
||||
return;
|
||||
}
|
||||
case 'trend': {
|
||||
const rows = readTrend(args[0], { limit: args[1] ? Number(args[1]) : 5 });
|
||||
process.stdout.write(JSON.stringify(rows, null, 2) + '\n');
|
||||
return;
|
||||
}
|
||||
default:
|
||||
process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend> [args]\n');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main(process.argv.slice(2));
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import path from 'node:path';
|
||||
|
||||
export const IMPECCABLE_DIR = '.impeccable';
|
||||
export const LIVE_DIR = 'live';
|
||||
export const CRITIQUE_DIR = 'critique';
|
||||
|
||||
export function getImpeccableDir(cwd = process.cwd()) {
|
||||
return path.join(cwd, IMPECCABLE_DIR);
|
||||
@@ -96,6 +97,10 @@ export function getLiveAnnotationsDir(cwd = process.cwd()) {
|
||||
return path.join(getLiveDir(cwd), 'annotations');
|
||||
}
|
||||
|
||||
export function getCritiqueDir(cwd = process.cwd()) {
|
||||
return path.join(getImpeccableDir(cwd), CRITIQUE_DIR);
|
||||
}
|
||||
|
||||
export function getLegacyLiveAnnotationsDir(cwd = process.cwd()) {
|
||||
return path.join(cwd, '.impeccable-live', 'annotations');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 3.0.7
|
||||
version: 3.1.0
|
||||
user-invocable: true
|
||||
argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · teach|document|extract|live] [target]"
|
||||
license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution.
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# Codex: Visual Direction & Asset Production
|
||||
|
||||
This file is loaded by `/impeccable craft` when the harness has native image generation (currently Codex via `image_gen`). Other harnesses skip it. It covers the two craft steps that depend on real image generation: landing the visual direction, and producing the raster assets the implementation will compose.
|
||||
|
||||
Read this *before* generating any images. The order matters, and the per-step user pauses are what keep generated imagery from drifting away from the brief.
|
||||
|
||||
## Step A: Explore Directions with the User
|
||||
|
||||
Before generating anything, run a brief direction conversation grounded in the shape brief.
|
||||
|
||||
Ask **2-3 targeted questions** about visual lane, color strategy, atmosphere, and named anchor references. Don't enumerate generic menus; tie each question to the shape brief's answers. Example shape-grounded questions:
|
||||
|
||||
- "Brief says 'editorial restraint, Klim-adjacent.' Are we closer to a quiet specimen page or a magazine-spread feel with hero imagery?"
|
||||
- "Palette strategy from shape was 'Committed.' Want it warm-grounded (deep oxblood + cream) or cool-grounded (slate + paper white)?"
|
||||
|
||||
Stop and wait for answers. These pin the palette before any pixel gets generated.
|
||||
|
||||
## Step B: Generate the Brand Palette First
|
||||
|
||||
Generate **one** palette artifact before any mocks. This is a small, focused image: typography pairing on the chosen background, primary + accent color swatches, one signature ornament or motif. Single image, single pass.
|
||||
|
||||
Why palette first: mocks generated against a vague color sense produce noise that drowns out the structural decisions. A confirmed palette is the first concrete contract for everything downstream.
|
||||
|
||||
Show the palette to the user. Ask one question: "This is the palette I'm locking in for the mocks. Confirm, or call out what to shift?"
|
||||
|
||||
Wait for confirmation. Do not generate mocks against an unconfirmed palette.
|
||||
|
||||
## Step C: Generate 1-3 Visual Mocks Against the Palette
|
||||
|
||||
Once the palette is confirmed, generate **1 to 3** high-fidelity north-star comps. Each mock must use the confirmed palette and typography. Mocks differ in *structural* direction (hierarchy, topology, density, composition), not in color or motif.
|
||||
|
||||
- Brand work: push visual identity, composition, mood, and signature motifs.
|
||||
- Product work: push hierarchy, topology, density, tone, grounded in realistic product structure.
|
||||
- Landing pages and long-form brand surfaces: show enough of the second fold to establish the system beyond the hero.
|
||||
|
||||
Use the `image_gen` tool directly (or via the imagegen skill when available). Don't ask the user to install anything.
|
||||
|
||||
## Step D: Approval Loop
|
||||
|
||||
Show the comps. Ask what carries forward. Iterate until **one direction is approved** or the user explicitly delegates.
|
||||
|
||||
If the user delegates, pick the strongest direction and explain it from the brief, not personal taste.
|
||||
|
||||
Before moving to assets, summarize what to carry into code and what *not* to literalize from the mock. This is the handoff between visual exploration and semantic implementation.
|
||||
|
||||
## Step E: Mock Fidelity Inventory
|
||||
|
||||
Inventory the approved mock's major visible ingredients. For each, decide implementation: semantic HTML/CSS/SVG, generated raster, sourced raster, icon library, canvas/WebGL, or accepted omission.
|
||||
|
||||
Common ingredients to inventory:
|
||||
|
||||
- Hero silhouette and dominant composition
|
||||
- Signature motifs (planets, devices, portraits, charts, route lines, insets, badges, etc.)
|
||||
- Nav and primary CTA treatment
|
||||
- Section sequence, especially the second fold
|
||||
- Image-native content the concept depends on
|
||||
- Typography, density, color/material treatment, motion cues
|
||||
|
||||
Treat the mock as a north star, not a screenshot to trace. Don't rasterize core UI text. But if the live result lacks the mock's major ingredients, the implementation is wrong.
|
||||
|
||||
If a photographic, architectural, product, or place-led mock becomes generic CSS scenery, decorative diagrams, bullets, or copy, stop and fix it. That's a broken implementation, not a harmless interpretation.
|
||||
|
||||
Don't substitute a different hero composition or visual driver post-approval without user sign-off.
|
||||
|
||||
## Step F: Asset Slicing via the Asset Producer
|
||||
|
||||
Raster ingredients identified in Step E need clean production assets. Use the bundled `impeccable_asset_producer` subagent rather than producing inline.
|
||||
|
||||
Spawn it as a scoped subagent. If you do not have explicit permission to use agents, stop and ask:
|
||||
|
||||
```text
|
||||
Asset production will work better as a scoped subagent job. Should I spawn the Impeccable asset producer subagent for this step?
|
||||
```
|
||||
|
||||
Pass to the agent:
|
||||
|
||||
- Approved mock path or screenshot reference
|
||||
- Crop paths or a contact sheet with crop ids
|
||||
- Output directory
|
||||
- Required dimensions, format, transparency needs
|
||||
- Avoid list
|
||||
- Notes on what should remain semantic HTML/CSS/SVG instead of raster
|
||||
|
||||
Attach image generation capability to the spawned agent when the harness supports it. Do **not** load image-generation reference material into the parent thread.
|
||||
|
||||
Inline asset production is allowed only if the user declines subagents, the harness cannot spawn the authorized agent, or the user explicitly asks for single-thread mode.
|
||||
|
||||
Prefer HTML/CSS/SVG/canvas when they can credibly reproduce an ingredient; reach for real, generated, or stock imagery when the mock or subject matter calls for actual visual content.
|
||||
|
||||
## After This File
|
||||
|
||||
Once Steps A through F are complete, return to `craft.md` Step 5 (Build to Production Quality). The implementation builds against the confirmed palette, approved mock, and the assets the producer wrote.
|
||||
@@ -51,64 +51,15 @@ Then add references based on the brief's needs:
|
||||
- Responsive requirements? Consult [responsive-design.md](responsive-design.md)
|
||||
- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.md)
|
||||
|
||||
## Step 3: Land the Visual Direction (Capability-Gated)
|
||||
## Step 3: Visual Direction & Assets (Harness-Gated)
|
||||
|
||||
Generate high-fidelity visual comps before implementation when all three are true:
|
||||
If the harness has **native image generation** (currently Codex via `image_gen`), this step is mandatory. **Stop and load [codex.md](codex.md)**. It covers palette generation, mock exploration, the approval loop, mock-fidelity inventory, and asset slicing via the `impeccable_asset_producer` subagent. Follow Steps A-F in that file, then return here for Step 4.
|
||||
|
||||
- The work is net-new or visually open-ended enough that composition exploration will improve the build.
|
||||
- The brief's scope is mid-fi, high-fi, or production-ready.
|
||||
- The harness gives you native image generation (Codex's `image_gen`, an equivalent MCP tool). Don't ask the user to set up external APIs.
|
||||
If the harness lacks native image generation, **state in one line that the visual-direction-by-generation step is being skipped because the harness lacks native image generation, then proceed**. The one-line announcement is required; it forces a conscious decision instead of letting the step quietly evaporate. The brief is your only visual reference. Implement directly from it, treating any named anchor references and the brief's "Design Direction" as the contract.
|
||||
|
||||
When met, this step is mandatory for both brand and product work. If image generation isn't available, skip silently. "The eventual UI is semantic/code-native/accessible" is not a reason to skip; those are implementation requirements, not exploration ones.
|
||||
Whether you generated mocks or not: don't replace required imagery with generic cards, bullets, emoji, fake metrics, decorative CSS panels, or filler copy. Image-led briefs (restaurants, hotels, magazines, photography, hobbyist communities, food, travel, fashion, product) need real or sourced imagery in the build, not CSS scenery.
|
||||
|
||||
### What to generate
|
||||
|
||||
Generate **1 to 3** high-fidelity north-star comps from the confirmed brief. If shape already produced direction probes, resolve the winning lane further, not an unrelated exploration. Comps must differ in primary visual direction, not just color.
|
||||
|
||||
- Brand work: push visual identity, composition, and mood aggressively.
|
||||
- Product work: push hierarchy, topology, density, tone, grounded in realistic product structure.
|
||||
- Landing pages and long-form brand surfaces: show enough of the second fold to establish the system beyond the hero.
|
||||
|
||||
### Approval loop
|
||||
|
||||
Show the comps and ask what carries forward. Iterate until one direction is approved or the user delegates. If the user delegates, pick the strongest direction and explain it from the brief, not personal taste.
|
||||
|
||||
Before Step 4, summarize what to carry into code and what **not** to literalize from the mock. This is the handoff between visual exploration and semantic implementation.
|
||||
|
||||
### Mock fidelity inventory
|
||||
|
||||
Inventory the approved mock's major visible ingredients:
|
||||
|
||||
- Hero silhouette and dominant composition
|
||||
- Signature motifs (planets, devices, portraits, charts, route lines, insets, badges, etc.)
|
||||
- Nav and primary CTA treatment
|
||||
- Section sequence, especially the second fold
|
||||
- Image-native content the concept depends on
|
||||
- Typography, density, color/material treatment, motion cues
|
||||
|
||||
For each, decide implementation: semantic HTML/CSS/SVG, generated asset, sourced asset, icon library, canvas/WebGL, or accepted omission. Don't substitute a different hero composition or visual driver post-approval without user sign-off.
|
||||
|
||||
If a photographic, architectural, product, or place-led mock becomes generic CSS scenery / decorative diagrams / bullets / copy, stop and fix it. That's a broken implementation, not a harmless interpretation.
|
||||
|
||||
Treat the mock as a north star, not a screenshot to trace. Don't rasterize core UI text. But if the live result lacks the mock's major ingredients, the implementation is wrong.
|
||||
|
||||
## Step 4: Asset Extraction (Need-Gated)
|
||||
|
||||
If the approved direction needs raster assets, create them before building. Do not replace required imagery with generic cards, bullets, emoji, fake metrics, decorative CSS panels, or filler copy.
|
||||
|
||||
Use the native asset producer (`impeccable_asset_producer` in Codex, `impeccable-asset-producer` in Claude Code) to create clean assets from the hi-fi mock and crops. If you do not have explicit permission to use agents, stop and ask:
|
||||
|
||||
```text
|
||||
Asset production will work better as a scoped subagent job. Should I spawn the Impeccable asset producer subagent for this step?
|
||||
```
|
||||
|
||||
Do not skip asset production or silently do it inline. Inline asset production is allowed only if the user declines subagents, the harness cannot spawn the authorized agent, or the user explicitly asks for single-thread mode.
|
||||
|
||||
Pass the approved mock, crop/contact-sheet paths, output directory, dimensions/formats, transparency needs, constraints, and avoid list to the asset producer. Attach image generation capability to the spawned agent when the harness supports it; do not load image-generation reference material into the parent thread first.
|
||||
|
||||
Prefer HTML/CSS/SVG/canvas when they can credibly reproduce an ingredient; use real/generated/stock imagery when the mock or subject matter calls for actual visual content.
|
||||
|
||||
## Step 5: Build to Production Quality
|
||||
## Step 4: Build to Production Quality
|
||||
|
||||
Implement the feature following the design brief. Build in passes so structure, visual system, states, motion/media, and responsive behavior each get deliberate attention. The list below is the definition of done, not inspiration.
|
||||
|
||||
@@ -130,7 +81,7 @@ Implement the feature following the design brief. Build in passes so structure,
|
||||
- **Technically clean.** Production build passes, no console errors, no avoidable layout shift, no needless dependencies, no broken asset paths.
|
||||
- **Ask when uncertain.** If a discovery materially changes the brief or approved direction, stop and ask. Don't guess.
|
||||
|
||||
## Step 6: Iterate Visually
|
||||
## Step 5: Iterate Visually
|
||||
|
||||
Look at what you built like a designer would. Your eyes are whatever the harness gives you: a connected browser, a screenshotting tool, Playwright, or asking the user. Use them for responsive testing (mobile, tablet, desktop minimum) and general visual validation.
|
||||
|
||||
@@ -144,7 +95,7 @@ Actively check: responsive behavior (composes, not shrinks), every state (empty
|
||||
|
||||
Detector or QA output is defect evidence only; never proof the work is finished.
|
||||
|
||||
## Step 7: Present
|
||||
## Step 6: Present
|
||||
|
||||
Present the result to the user:
|
||||
- Show the feature in its primary state
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
> **Additional context needed**: what the interface is trying to accomplish.
|
||||
|
||||
### Setup: Resolve Target and Load Ignore List
|
||||
|
||||
Before gathering assessments, do two small bookkeeping steps. They cost almost nothing and they're what makes critique iterative across runs.
|
||||
|
||||
1. **Resolve the primary artifact.** The user's phrasing ("the homepage", "the pricing flow") is not stable enough to track across runs. Resolve it to a concrete file path or URL: the same one you'd already need to scan code or open in a browser. Examples:
|
||||
- "the homepage" → `site/pages/index.astro` (or `http://localhost:3000/` if you're inspecting live)
|
||||
- "the settings modal" → the primary component file (e.g., `src/components/Settings.tsx`)
|
||||
- "this page" → the URL or the page's source file
|
||||
Prefer the source file path over the dev-server URL when both exist; ports drift between runs (`bun dev` vs `bun preview`), file paths don't.
|
||||
|
||||
2. **Compute the slug.** Run:
|
||||
```bash
|
||||
node .github/skills/impeccable/scripts/critique-storage.mjs slug "<resolved-path-or-url>"
|
||||
```
|
||||
Keep the printed slug. It identifies this target's stream across runs. If the command exits non-zero ("no stable slug for input"), skip persistence for this run and tell the user; the trend won't update but the critique still goes ahead.
|
||||
|
||||
3. **Read the ignore list** at `.impeccable/critique/ignore.md` if it exists. Plain markdown; each non-empty, non-comment line is something the user has marked as "do not re-raise" (deferred tradeoffs, designer-intended deviations, detector false-positives the user accepts). When a finding's text matches a line here (case-insensitive substring against rule name or snippet), **drop it silently**. Do not mention it in the report. This is the ONLY input critique consumes from prior runs; anchoring on prior findings would defeat the point of independent assessment.
|
||||
|
||||
### Gather Assessments
|
||||
|
||||
Launch two independent assessments. **Neither may see the other's output.** This isolation is what makes the combined score honest. Running both in one head silently anchors them to each other; do not shortcut it for cost, speed, or context-size reasons.
|
||||
@@ -164,6 +182,36 @@ Provocative questions that might unlock better solutions:
|
||||
- Prioritize ruthlessly. If everything is important, nothing is.
|
||||
- Don't soften criticism. Developers need honest feedback to ship great design.
|
||||
|
||||
### Persist the Snapshot
|
||||
|
||||
Once the report above is finalized, write it to `.impeccable/critique/` so the user can refer back, and so `/impeccable polish` can pick up the priority issues without a copy-paste.
|
||||
|
||||
Skip this step if the Setup slug was null (vague or root-level target).
|
||||
|
||||
1. **Write the body to a temp file** so you can pipe it to the helper. Use the full report (heuristic table, anti-patterns verdict, priority issues, persona red flags) but stop before the "Ask the User" / "Recommended Actions" sections that come later.
|
||||
|
||||
2. **Pass the structured metadata** through `IMPECCABLE_CRITIQUE_META` (JSON), then run the write command:
|
||||
```bash
|
||||
IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"p0_count":<n>,"p1_count":<n>}' \
|
||||
node .github/skills/impeccable/scripts/critique-storage.mjs write <slug> <body-file>
|
||||
```
|
||||
The helper prints the absolute path it wrote.
|
||||
|
||||
3. **Read the trend** for context:
|
||||
```bash
|
||||
node .github/skills/impeccable/scripts/critique-storage.mjs trend <slug> 5
|
||||
```
|
||||
This returns a JSON array of the last 5 frontmatter entries (including the one you just wrote).
|
||||
|
||||
4. **Append a single line to the user-visible output**, after the report and before the questions:
|
||||
|
||||
> **Trend for `<slug>` (last 5 runs): 24 → 28 → 32 → 29 → 32**
|
||||
> Wrote `.impeccable/critique/<filename>`.
|
||||
|
||||
If this is the first run for the slug, the trend is just one score; say so: "First run for this target, no trend yet."
|
||||
|
||||
This is fire-and-forget. Do not show the user the helper's JSON output; only the human-readable trend line and the written path. Failures here should not block the rest of the flow; print the error and move on.
|
||||
|
||||
### Ask the User
|
||||
|
||||
**After presenting findings**, use targeted questions based on what was actually found. ask the user directly to clarify what you cannot infer. These answers will shape the action plan.
|
||||
|
||||
@@ -35,7 +35,14 @@ Understand the current state and goals before touching anything:
|
||||
- Loading and transition smoothness
|
||||
- Information architecture and flow drift (does this feature reveal complexity the way neighboring features do?)
|
||||
|
||||
4. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent; never perfect one corner while leaving another rough.
|
||||
4. **Pull in any prior critique** (optional signal): If `/impeccable critique` has been run on the same target, its priority issues are a useful prior for what to address first. Resolve the target to a file path or URL, then:
|
||||
```bash
|
||||
slug=$(node .github/skills/impeccable/scripts/critique-storage.mjs slug "<resolved>")
|
||||
node .github/skills/impeccable/scripts/critique-storage.mjs latest "$slug"
|
||||
```
|
||||
Exit 0 with body = found; fold the P0/P1 items into your polish list and mention the snapshot path so the user sees what you read. Exit 2 = no snapshot, continue without it. The critique is one input among many. Do your own pass either way.
|
||||
|
||||
5. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent; never perfect one corner while leaving another rough.
|
||||
|
||||
**CRITICAL**: Polish is the last step, not the first. Don't polish work that's not functionally complete.
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ After the discovery interview, generate a small set of visual direction probes *
|
||||
- The requested fidelity is **mid-fi, high-fi, or production-ready**. Skip for sketch-only planning.
|
||||
- The current harness gives you native image generation (Codex's `image_gen`, an equivalent MCP tool, or similar). Don't ask the user to install APIs or tooling.
|
||||
|
||||
When those conditions are met, this step is mandatory. If image generation isn't natively available, skip silently and proceed; don't announce the skip.
|
||||
When those conditions are met, this step is mandatory. If image generation isn't natively available, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed. The one-line announcement is required, not optional; it forces a conscious decision instead of letting the step quietly evaporate.
|
||||
|
||||
Use probes to explore visual lanes, not to replace the brief.
|
||||
|
||||
@@ -107,7 +107,7 @@ The probes should differ in primary visual direction (hierarchy, topology, densi
|
||||
- Do **not** treat generated imagery as final UX specification, final copy, or final accessibility behavior.
|
||||
- Do **not** use this step for minor refinements of existing work. It's for shaping a new surface or clarifying a big directional choice.
|
||||
|
||||
If image generation isn't natively available, skip this phase silently and proceed.
|
||||
If image generation isn't natively available, announce the skip in one line and proceed to the design brief.
|
||||
|
||||
## Phase 2: Design Brief
|
||||
|
||||
@@ -120,7 +120,7 @@ After the interview and any required probes, present a brief and **end your resp
|
||||
|
||||
Don't pad a clear brief into a long one to look thorough. A 70-line brief restating answers the user just gave is noise, not rigor. Equally, don't skip the confirmation pause to look efficient: the pause is the point.
|
||||
|
||||
If the user already said "approved" or "go" during discovery for the *exact direction you'd present*, that counts as confirmation; you may proceed without asking again. But if your brief adds anything the user hasn't seen and approved, you must stop and confirm.
|
||||
Present the brief, then **stop and wait for explicit confirmation**. You are not the judge of whether the user already approved. Even when the brief feels obviously right, ask once and wait. The pause is what separates shape from premature implementation.
|
||||
|
||||
### Brief Structure
|
||||
|
||||
@@ -158,8 +158,8 @@ Anything genuinely unresolved. Don't list "open questions" you've already recomm
|
||||
|
||||
---
|
||||
|
||||
ask the user directly to clarify what you cannot infer.
|
||||
ask the user directly to clarify what you cannot infer. Ask for explicit confirmation of the brief before finishing.
|
||||
|
||||
If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the user confirms direction (or the user already gave a clear go during discovery, which counts).
|
||||
If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the user confirms direction.
|
||||
|
||||
Once confirmed, the brief is complete. The user can now hand it to /impeccable, or use it to guide any other implementation approach. (If the user wants the full discovery-then-build flow in one step, they should use /impeccable craft instead, which runs this command internally.)
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Critique persistence helper.
|
||||
*
|
||||
* Each run of /impeccable critique writes a per-target snapshot to
|
||||
* .impeccable/critique/<timestamp>__<slug>.md
|
||||
* with a small YAML frontmatter carrying the score + P0/P1 counts.
|
||||
*
|
||||
* /impeccable polish reads the latest matching snapshot at start as its
|
||||
* fix backlog. No other skill auto-reads critique output.
|
||||
*
|
||||
* The slug is derived mechanically from the *resolved* primary artifact
|
||||
* (file path or URL), never from the user's natural-language phrasing.
|
||||
* Slug stability across runs is what lets the trend display work.
|
||||
*
|
||||
* CLI entry points (called from skill instructions):
|
||||
* node critique-storage.mjs slug <resolved-target>
|
||||
* node critique-storage.mjs write <slug> <snapshot-body-file>
|
||||
* node critique-storage.mjs latest <slug>
|
||||
* node critique-storage.mjs trend <slug> [limit]
|
||||
*
|
||||
* Note: there is intentionally no `ignore` subcommand. ignore.md is a plain
|
||||
* markdown file; the model reads it directly with its file-read tool. This
|
||||
* helper only exists for operations the model can't trivially do inline
|
||||
* (normalizing paths, generating filenames, globbing + parsing frontmatter).
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { getCritiqueDir } from './impeccable-paths.mjs';
|
||||
|
||||
const SLUG_MAX = 50;
|
||||
|
||||
/**
|
||||
* Mechanically derive a slug from a resolved target. Returns null if the
|
||||
* input doesn't look like a stable identifier (empty, project root, etc).
|
||||
*
|
||||
* Accepts file paths and URLs. The model resolves "the homepage" to a
|
||||
* concrete artifact before calling this — we never slug a natural-language
|
||||
* phrase.
|
||||
*/
|
||||
export function slugFromTarget(resolved, { cwd = process.cwd() } = {}) {
|
||||
if (!resolved || typeof resolved !== 'string') return null;
|
||||
const trimmed = resolved.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
// URL
|
||||
if (/^https?:\/\//i.test(trimmed)) {
|
||||
let url;
|
||||
try { url = new URL(trimmed); } catch { return null; }
|
||||
const hostPath = `${url.hostname}${url.pathname}`;
|
||||
return kebab(hostPath);
|
||||
}
|
||||
|
||||
// File path. Make it project-relative so two devs critiquing the same
|
||||
// checkout get the same slug regardless of where their repo is cloned.
|
||||
const abs = path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
|
||||
let rel = path.relative(cwd, abs);
|
||||
// If the target is outside cwd, fall back to the basename so we still
|
||||
// produce a stable slug (vs the absolute path, which would include
|
||||
// home dirs / usernames).
|
||||
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
||||
rel = path.basename(abs);
|
||||
}
|
||||
if (!rel || rel === '.' || rel === '') return null;
|
||||
return kebab(rel);
|
||||
}
|
||||
|
||||
function kebab(s) {
|
||||
const slug = s
|
||||
.toLowerCase()
|
||||
.replace(/[/\\.]+/g, '-')
|
||||
.replace(/[^a-z0-9-]+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
if (!slug) return null;
|
||||
// Cap from the tail — the tail (filename) is more identifying than the
|
||||
// top-level directory.
|
||||
return slug.length <= SLUG_MAX ? slug : slug.slice(slug.length - SLUG_MAX).replace(/^-/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filename-safe UTC ISO timestamp: hyphens for separators, trailing Z.
|
||||
* Plain colons aren't allowed on Windows filesystems.
|
||||
*/
|
||||
export function nowFilenameStamp(date = new Date()) {
|
||||
const iso = date.toISOString(); // 2026-05-12T18:30:00.123Z
|
||||
return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z');
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a snapshot for `slug`. `meta` carries the small structured frontmatter
|
||||
* keys read back by readTrend(). `body` is the human-readable critique
|
||||
* report (everything below the frontmatter).
|
||||
*
|
||||
* Returns the absolute path written.
|
||||
*/
|
||||
export function writeSnapshot({ slug, meta, body, cwd = process.cwd(), now = new Date() }) {
|
||||
if (!slug) throw new Error('writeSnapshot requires a slug');
|
||||
const dir = getCritiqueDir(cwd);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const timestamp = nowFilenameStamp(now);
|
||||
const filePath = path.join(dir, `${timestamp}__${slug}.md`);
|
||||
// Spread `meta` first so internally computed `timestamp` and `slug`
|
||||
// always win. Otherwise a caller-supplied meta blob (parsed from the
|
||||
// IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the
|
||||
// filename in disagreement with its frontmatter and corrupting trends.
|
||||
const front = serializeFrontmatter({ ...meta, timestamp, slug });
|
||||
fs.writeFileSync(filePath, `${front}\n${body.trim()}\n`, 'utf-8');
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function serializeFrontmatter(obj) {
|
||||
const lines = ['---'];
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
const str = typeof value === 'string' ? value : String(value);
|
||||
// Quote strings that contain : or # to keep parsing simple.
|
||||
const needsQuotes = typeof value === 'string' && /[:#]/.test(str);
|
||||
lines.push(`${key}: ${needsQuotes ? JSON.stringify(str) : str}`);
|
||||
}
|
||||
lines.push('---');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function parseFrontmatter(text) {
|
||||
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
if (!match) return {};
|
||||
const out = {};
|
||||
for (const line of match[1].split(/\r?\n/)) {
|
||||
const colon = line.indexOf(':');
|
||||
if (colon < 0) continue;
|
||||
const key = line.slice(0, colon).trim();
|
||||
let value = line.slice(colon + 1).trim();
|
||||
if (/^".*"$/.test(value)) {
|
||||
try { value = JSON.parse(value); } catch { /* leave as-is */ }
|
||||
} else if (/^-?\d+$/.test(value)) {
|
||||
value = Number(value);
|
||||
}
|
||||
out[key] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all snapshot files for `slug`, sorted oldest → newest.
|
||||
*/
|
||||
function listSnapshotsForSlug(slug, cwd) {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const suffix = `__${slug}.md`;
|
||||
return fs.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(suffix))
|
||||
.sort()
|
||||
.map((f) => path.join(dir, f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent snapshot for `slug`, or null. Polish reads this
|
||||
* to find its fix backlog when the slug matches.
|
||||
*/
|
||||
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
if (!all.length) return null;
|
||||
const latest = all[all.length - 1];
|
||||
const body = fs.readFileSync(latest, 'utf-8');
|
||||
return { path: latest, body, meta: parseFrontmatter(body) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last `limit` snapshots' frontmatter, oldest → newest.
|
||||
* Critique appends a one-line trend to its output using this.
|
||||
*/
|
||||
export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
const slice = all.slice(-limit);
|
||||
return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8')));
|
||||
}
|
||||
|
||||
// ---- CLI ---------------------------------------------------------------
|
||||
|
||||
function main(argv) {
|
||||
const [cmd, ...args] = argv;
|
||||
switch (cmd) {
|
||||
case 'slug': {
|
||||
const slug = slugFromTarget(args[0]);
|
||||
if (!slug) { process.stderr.write('no stable slug for input\n'); process.exit(1); }
|
||||
process.stdout.write(`${slug}\n`);
|
||||
return;
|
||||
}
|
||||
case 'write': {
|
||||
const [slug, bodyFile] = args;
|
||||
if (!slug || !bodyFile) { process.stderr.write('usage: write <slug> <body-file>\n'); process.exit(1); }
|
||||
const raw = fs.readFileSync(bodyFile, 'utf-8');
|
||||
// The body file may be a full report. The caller passes the meta as
|
||||
// a JSON object on stdin if it wants structured frontmatter; otherwise
|
||||
// we write with minimal metadata.
|
||||
let meta = {};
|
||||
const metaArg = process.env.IMPECCABLE_CRITIQUE_META;
|
||||
if (metaArg) {
|
||||
try { meta = JSON.parse(metaArg); } catch { /* ignore */ }
|
||||
}
|
||||
const out = writeSnapshot({ slug, meta, body: raw });
|
||||
process.stdout.write(`${out}\n`);
|
||||
return;
|
||||
}
|
||||
case 'latest': {
|
||||
const latest = readLatestSnapshot(args[0]);
|
||||
if (!latest) { process.exit(2); }
|
||||
process.stdout.write(latest.body);
|
||||
return;
|
||||
}
|
||||
case 'trend': {
|
||||
const rows = readTrend(args[0], { limit: args[1] ? Number(args[1]) : 5 });
|
||||
process.stdout.write(JSON.stringify(rows, null, 2) + '\n');
|
||||
return;
|
||||
}
|
||||
default:
|
||||
process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend> [args]\n');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main(process.argv.slice(2));
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import path from 'node:path';
|
||||
|
||||
export const IMPECCABLE_DIR = '.impeccable';
|
||||
export const LIVE_DIR = 'live';
|
||||
export const CRITIQUE_DIR = 'critique';
|
||||
|
||||
export function getImpeccableDir(cwd = process.cwd()) {
|
||||
return path.join(cwd, IMPECCABLE_DIR);
|
||||
@@ -96,6 +97,10 @@ export function getLiveAnnotationsDir(cwd = process.cwd()) {
|
||||
return path.join(getLiveDir(cwd), 'annotations');
|
||||
}
|
||||
|
||||
export function getCritiqueDir(cwd = process.cwd()) {
|
||||
return path.join(getImpeccableDir(cwd), CRITIQUE_DIR);
|
||||
}
|
||||
|
||||
export function getLegacyLiveAnnotationsDir(cwd = process.cwd()) {
|
||||
return path.join(cwd, '.impeccable-live', 'annotations');
|
||||
}
|
||||
|
||||
@@ -44,6 +44,11 @@ Thumbs.db
|
||||
.impeccable/live/annotations/
|
||||
.impeccable/live/cache/
|
||||
.impeccable/history/
|
||||
# Per-run critique snapshots are local artifacts. ignore.md (also under
|
||||
# this dir) carries deferrals the user may want to share, so it's
|
||||
# explicitly re-included below.
|
||||
.impeccable/critique/
|
||||
!.impeccable/critique/ignore.md
|
||||
|
||||
# Legacy live mode session file + annotation screenshots
|
||||
.impeccable-live.json
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 3.0.7
|
||||
version: 3.1.0
|
||||
license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution.
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# Codex: Visual Direction & Asset Production
|
||||
|
||||
This file is loaded by `/impeccable craft` when the harness has native image generation (currently Codex via `image_gen`). Other harnesses skip it. It covers the two craft steps that depend on real image generation: landing the visual direction, and producing the raster assets the implementation will compose.
|
||||
|
||||
Read this *before* generating any images. The order matters, and the per-step user pauses are what keep generated imagery from drifting away from the brief.
|
||||
|
||||
## Step A: Explore Directions with the User
|
||||
|
||||
Before generating anything, run a brief direction conversation grounded in the shape brief.
|
||||
|
||||
Ask **2-3 targeted questions** about visual lane, color strategy, atmosphere, and named anchor references. Don't enumerate generic menus; tie each question to the shape brief's answers. Example shape-grounded questions:
|
||||
|
||||
- "Brief says 'editorial restraint, Klim-adjacent.' Are we closer to a quiet specimen page or a magazine-spread feel with hero imagery?"
|
||||
- "Palette strategy from shape was 'Committed.' Want it warm-grounded (deep oxblood + cream) or cool-grounded (slate + paper white)?"
|
||||
|
||||
Stop and wait for answers. These pin the palette before any pixel gets generated.
|
||||
|
||||
## Step B: Generate the Brand Palette First
|
||||
|
||||
Generate **one** palette artifact before any mocks. This is a small, focused image: typography pairing on the chosen background, primary + accent color swatches, one signature ornament or motif. Single image, single pass.
|
||||
|
||||
Why palette first: mocks generated against a vague color sense produce noise that drowns out the structural decisions. A confirmed palette is the first concrete contract for everything downstream.
|
||||
|
||||
Show the palette to the user. Ask one question: "This is the palette I'm locking in for the mocks. Confirm, or call out what to shift?"
|
||||
|
||||
Wait for confirmation. Do not generate mocks against an unconfirmed palette.
|
||||
|
||||
## Step C: Generate 1-3 Visual Mocks Against the Palette
|
||||
|
||||
Once the palette is confirmed, generate **1 to 3** high-fidelity north-star comps. Each mock must use the confirmed palette and typography. Mocks differ in *structural* direction (hierarchy, topology, density, composition), not in color or motif.
|
||||
|
||||
- Brand work: push visual identity, composition, mood, and signature motifs.
|
||||
- Product work: push hierarchy, topology, density, tone, grounded in realistic product structure.
|
||||
- Landing pages and long-form brand surfaces: show enough of the second fold to establish the system beyond the hero.
|
||||
|
||||
Use the `image_gen` tool directly (or via the imagegen skill when available). Don't ask the user to install anything.
|
||||
|
||||
## Step D: Approval Loop
|
||||
|
||||
Show the comps. Ask what carries forward. Iterate until **one direction is approved** or the user explicitly delegates.
|
||||
|
||||
If the user delegates, pick the strongest direction and explain it from the brief, not personal taste.
|
||||
|
||||
Before moving to assets, summarize what to carry into code and what *not* to literalize from the mock. This is the handoff between visual exploration and semantic implementation.
|
||||
|
||||
## Step E: Mock Fidelity Inventory
|
||||
|
||||
Inventory the approved mock's major visible ingredients. For each, decide implementation: semantic HTML/CSS/SVG, generated raster, sourced raster, icon library, canvas/WebGL, or accepted omission.
|
||||
|
||||
Common ingredients to inventory:
|
||||
|
||||
- Hero silhouette and dominant composition
|
||||
- Signature motifs (planets, devices, portraits, charts, route lines, insets, badges, etc.)
|
||||
- Nav and primary CTA treatment
|
||||
- Section sequence, especially the second fold
|
||||
- Image-native content the concept depends on
|
||||
- Typography, density, color/material treatment, motion cues
|
||||
|
||||
Treat the mock as a north star, not a screenshot to trace. Don't rasterize core UI text. But if the live result lacks the mock's major ingredients, the implementation is wrong.
|
||||
|
||||
If a photographic, architectural, product, or place-led mock becomes generic CSS scenery, decorative diagrams, bullets, or copy, stop and fix it. That's a broken implementation, not a harmless interpretation.
|
||||
|
||||
Don't substitute a different hero composition or visual driver post-approval without user sign-off.
|
||||
|
||||
## Step F: Asset Slicing via the Asset Producer
|
||||
|
||||
Raster ingredients identified in Step E need clean production assets. Use the bundled `impeccable_asset_producer` subagent rather than producing inline.
|
||||
|
||||
Spawn it as a scoped subagent. If you do not have explicit permission to use agents, stop and ask:
|
||||
|
||||
```text
|
||||
Asset production will work better as a scoped subagent job. Should I spawn the Impeccable asset producer subagent for this step?
|
||||
```
|
||||
|
||||
Pass to the agent:
|
||||
|
||||
- Approved mock path or screenshot reference
|
||||
- Crop paths or a contact sheet with crop ids
|
||||
- Output directory
|
||||
- Required dimensions, format, transparency needs
|
||||
- Avoid list
|
||||
- Notes on what should remain semantic HTML/CSS/SVG instead of raster
|
||||
|
||||
Attach image generation capability to the spawned agent when the harness supports it. Do **not** load image-generation reference material into the parent thread.
|
||||
|
||||
Inline asset production is allowed only if the user declines subagents, the harness cannot spawn the authorized agent, or the user explicitly asks for single-thread mode.
|
||||
|
||||
Prefer HTML/CSS/SVG/canvas when they can credibly reproduce an ingredient; reach for real, generated, or stock imagery when the mock or subject matter calls for actual visual content.
|
||||
|
||||
## After This File
|
||||
|
||||
Once Steps A through F are complete, return to `craft.md` Step 5 (Build to Production Quality). The implementation builds against the confirmed palette, approved mock, and the assets the producer wrote.
|
||||
@@ -51,64 +51,15 @@ Then add references based on the brief's needs:
|
||||
- Responsive requirements? Consult [responsive-design.md](responsive-design.md)
|
||||
- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.md)
|
||||
|
||||
## Step 3: Land the Visual Direction (Capability-Gated)
|
||||
## Step 3: Visual Direction & Assets (Harness-Gated)
|
||||
|
||||
Generate high-fidelity visual comps before implementation when all three are true:
|
||||
If the harness has **native image generation** (currently Codex via `image_gen`), this step is mandatory. **Stop and load [codex.md](codex.md)**. It covers palette generation, mock exploration, the approval loop, mock-fidelity inventory, and asset slicing via the `impeccable_asset_producer` subagent. Follow Steps A-F in that file, then return here for Step 4.
|
||||
|
||||
- The work is net-new or visually open-ended enough that composition exploration will improve the build.
|
||||
- The brief's scope is mid-fi, high-fi, or production-ready.
|
||||
- The harness gives you native image generation (Codex's `image_gen`, an equivalent MCP tool). Don't ask the user to set up external APIs.
|
||||
If the harness lacks native image generation, **state in one line that the visual-direction-by-generation step is being skipped because the harness lacks native image generation, then proceed**. The one-line announcement is required; it forces a conscious decision instead of letting the step quietly evaporate. The brief is your only visual reference. Implement directly from it, treating any named anchor references and the brief's "Design Direction" as the contract.
|
||||
|
||||
When met, this step is mandatory for both brand and product work. If image generation isn't available, skip silently. "The eventual UI is semantic/code-native/accessible" is not a reason to skip; those are implementation requirements, not exploration ones.
|
||||
Whether you generated mocks or not: don't replace required imagery with generic cards, bullets, emoji, fake metrics, decorative CSS panels, or filler copy. Image-led briefs (restaurants, hotels, magazines, photography, hobbyist communities, food, travel, fashion, product) need real or sourced imagery in the build, not CSS scenery.
|
||||
|
||||
### What to generate
|
||||
|
||||
Generate **1 to 3** high-fidelity north-star comps from the confirmed brief. If shape already produced direction probes, resolve the winning lane further, not an unrelated exploration. Comps must differ in primary visual direction, not just color.
|
||||
|
||||
- Brand work: push visual identity, composition, and mood aggressively.
|
||||
- Product work: push hierarchy, topology, density, tone, grounded in realistic product structure.
|
||||
- Landing pages and long-form brand surfaces: show enough of the second fold to establish the system beyond the hero.
|
||||
|
||||
### Approval loop
|
||||
|
||||
Show the comps and ask what carries forward. Iterate until one direction is approved or the user delegates. If the user delegates, pick the strongest direction and explain it from the brief, not personal taste.
|
||||
|
||||
Before Step 4, summarize what to carry into code and what **not** to literalize from the mock. This is the handoff between visual exploration and semantic implementation.
|
||||
|
||||
### Mock fidelity inventory
|
||||
|
||||
Inventory the approved mock's major visible ingredients:
|
||||
|
||||
- Hero silhouette and dominant composition
|
||||
- Signature motifs (planets, devices, portraits, charts, route lines, insets, badges, etc.)
|
||||
- Nav and primary CTA treatment
|
||||
- Section sequence, especially the second fold
|
||||
- Image-native content the concept depends on
|
||||
- Typography, density, color/material treatment, motion cues
|
||||
|
||||
For each, decide implementation: semantic HTML/CSS/SVG, generated asset, sourced asset, icon library, canvas/WebGL, or accepted omission. Don't substitute a different hero composition or visual driver post-approval without user sign-off.
|
||||
|
||||
If a photographic, architectural, product, or place-led mock becomes generic CSS scenery / decorative diagrams / bullets / copy, stop and fix it. That's a broken implementation, not a harmless interpretation.
|
||||
|
||||
Treat the mock as a north star, not a screenshot to trace. Don't rasterize core UI text. But if the live result lacks the mock's major ingredients, the implementation is wrong.
|
||||
|
||||
## Step 4: Asset Extraction (Need-Gated)
|
||||
|
||||
If the approved direction needs raster assets, create them before building. Do not replace required imagery with generic cards, bullets, emoji, fake metrics, decorative CSS panels, or filler copy.
|
||||
|
||||
Use the native asset producer (`impeccable_asset_producer` in Codex, `impeccable-asset-producer` in Claude Code) to create clean assets from the hi-fi mock and crops. If you do not have explicit permission to use agents, stop and ask:
|
||||
|
||||
```text
|
||||
Asset production will work better as a scoped subagent job. Should I spawn the Impeccable asset producer subagent for this step?
|
||||
```
|
||||
|
||||
Do not skip asset production or silently do it inline. Inline asset production is allowed only if the user declines subagents, the harness cannot spawn the authorized agent, or the user explicitly asks for single-thread mode.
|
||||
|
||||
Pass the approved mock, crop/contact-sheet paths, output directory, dimensions/formats, transparency needs, constraints, and avoid list to the asset producer. Attach image generation capability to the spawned agent when the harness supports it; do not load image-generation reference material into the parent thread first.
|
||||
|
||||
Prefer HTML/CSS/SVG/canvas when they can credibly reproduce an ingredient; use real/generated/stock imagery when the mock or subject matter calls for actual visual content.
|
||||
|
||||
## Step 5: Build to Production Quality
|
||||
## Step 4: Build to Production Quality
|
||||
|
||||
Implement the feature following the design brief. Build in passes so structure, visual system, states, motion/media, and responsive behavior each get deliberate attention. The list below is the definition of done, not inspiration.
|
||||
|
||||
@@ -130,7 +81,7 @@ Implement the feature following the design brief. Build in passes so structure,
|
||||
- **Technically clean.** Production build passes, no console errors, no avoidable layout shift, no needless dependencies, no broken asset paths.
|
||||
- **Ask when uncertain.** If a discovery materially changes the brief or approved direction, stop and ask. Don't guess.
|
||||
|
||||
## Step 6: Iterate Visually
|
||||
## Step 5: Iterate Visually
|
||||
|
||||
Look at what you built like a designer would. Your eyes are whatever the harness gives you: a connected browser, a screenshotting tool, Playwright, or asking the user. Use them for responsive testing (mobile, tablet, desktop minimum) and general visual validation.
|
||||
|
||||
@@ -144,7 +95,7 @@ Actively check: responsive behavior (composes, not shrinks), every state (empty
|
||||
|
||||
Detector or QA output is defect evidence only; never proof the work is finished.
|
||||
|
||||
## Step 7: Present
|
||||
## Step 6: Present
|
||||
|
||||
Present the result to the user:
|
||||
- Show the feature in its primary state
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
> **Additional context needed**: what the interface is trying to accomplish.
|
||||
|
||||
### Setup: Resolve Target and Load Ignore List
|
||||
|
||||
Before gathering assessments, do two small bookkeeping steps. They cost almost nothing and they're what makes critique iterative across runs.
|
||||
|
||||
1. **Resolve the primary artifact.** The user's phrasing ("the homepage", "the pricing flow") is not stable enough to track across runs. Resolve it to a concrete file path or URL: the same one you'd already need to scan code or open in a browser. Examples:
|
||||
- "the homepage" → `site/pages/index.astro` (or `http://localhost:3000/` if you're inspecting live)
|
||||
- "the settings modal" → the primary component file (e.g., `src/components/Settings.tsx`)
|
||||
- "this page" → the URL or the page's source file
|
||||
Prefer the source file path over the dev-server URL when both exist; ports drift between runs (`bun dev` vs `bun preview`), file paths don't.
|
||||
|
||||
2. **Compute the slug.** Run:
|
||||
```bash
|
||||
node .kiro/skills/impeccable/scripts/critique-storage.mjs slug "<resolved-path-or-url>"
|
||||
```
|
||||
Keep the printed slug. It identifies this target's stream across runs. If the command exits non-zero ("no stable slug for input"), skip persistence for this run and tell the user; the trend won't update but the critique still goes ahead.
|
||||
|
||||
3. **Read the ignore list** at `.impeccable/critique/ignore.md` if it exists. Plain markdown; each non-empty, non-comment line is something the user has marked as "do not re-raise" (deferred tradeoffs, designer-intended deviations, detector false-positives the user accepts). When a finding's text matches a line here (case-insensitive substring against rule name or snippet), **drop it silently**. Do not mention it in the report. This is the ONLY input critique consumes from prior runs; anchoring on prior findings would defeat the point of independent assessment.
|
||||
|
||||
### Gather Assessments
|
||||
|
||||
Launch two independent assessments. **Neither may see the other's output.** This isolation is what makes the combined score honest. Running both in one head silently anchors them to each other; do not shortcut it for cost, speed, or context-size reasons.
|
||||
@@ -164,6 +182,36 @@ Provocative questions that might unlock better solutions:
|
||||
- Prioritize ruthlessly. If everything is important, nothing is.
|
||||
- Don't soften criticism. Developers need honest feedback to ship great design.
|
||||
|
||||
### Persist the Snapshot
|
||||
|
||||
Once the report above is finalized, write it to `.impeccable/critique/` so the user can refer back, and so `/impeccable polish` can pick up the priority issues without a copy-paste.
|
||||
|
||||
Skip this step if the Setup slug was null (vague or root-level target).
|
||||
|
||||
1. **Write the body to a temp file** so you can pipe it to the helper. Use the full report (heuristic table, anti-patterns verdict, priority issues, persona red flags) but stop before the "Ask the User" / "Recommended Actions" sections that come later.
|
||||
|
||||
2. **Pass the structured metadata** through `IMPECCABLE_CRITIQUE_META` (JSON), then run the write command:
|
||||
```bash
|
||||
IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"p0_count":<n>,"p1_count":<n>}' \
|
||||
node .kiro/skills/impeccable/scripts/critique-storage.mjs write <slug> <body-file>
|
||||
```
|
||||
The helper prints the absolute path it wrote.
|
||||
|
||||
3. **Read the trend** for context:
|
||||
```bash
|
||||
node .kiro/skills/impeccable/scripts/critique-storage.mjs trend <slug> 5
|
||||
```
|
||||
This returns a JSON array of the last 5 frontmatter entries (including the one you just wrote).
|
||||
|
||||
4. **Append a single line to the user-visible output**, after the report and before the questions:
|
||||
|
||||
> **Trend for `<slug>` (last 5 runs): 24 → 28 → 32 → 29 → 32**
|
||||
> Wrote `.impeccable/critique/<filename>`.
|
||||
|
||||
If this is the first run for the slug, the trend is just one score; say so: "First run for this target, no trend yet."
|
||||
|
||||
This is fire-and-forget. Do not show the user the helper's JSON output; only the human-readable trend line and the written path. Failures here should not block the rest of the flow; print the error and move on.
|
||||
|
||||
### Ask the User
|
||||
|
||||
**After presenting findings**, use targeted questions based on what was actually found. ask the user directly to clarify what you cannot infer. These answers will shape the action plan.
|
||||
|
||||
@@ -35,7 +35,14 @@ Understand the current state and goals before touching anything:
|
||||
- Loading and transition smoothness
|
||||
- Information architecture and flow drift (does this feature reveal complexity the way neighboring features do?)
|
||||
|
||||
4. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent; never perfect one corner while leaving another rough.
|
||||
4. **Pull in any prior critique** (optional signal): If `/impeccable critique` has been run on the same target, its priority issues are a useful prior for what to address first. Resolve the target to a file path or URL, then:
|
||||
```bash
|
||||
slug=$(node .kiro/skills/impeccable/scripts/critique-storage.mjs slug "<resolved>")
|
||||
node .kiro/skills/impeccable/scripts/critique-storage.mjs latest "$slug"
|
||||
```
|
||||
Exit 0 with body = found; fold the P0/P1 items into your polish list and mention the snapshot path so the user sees what you read. Exit 2 = no snapshot, continue without it. The critique is one input among many. Do your own pass either way.
|
||||
|
||||
5. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent; never perfect one corner while leaving another rough.
|
||||
|
||||
**CRITICAL**: Polish is the last step, not the first. Don't polish work that's not functionally complete.
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ After the discovery interview, generate a small set of visual direction probes *
|
||||
- The requested fidelity is **mid-fi, high-fi, or production-ready**. Skip for sketch-only planning.
|
||||
- The current harness gives you native image generation (Codex's `image_gen`, an equivalent MCP tool, or similar). Don't ask the user to install APIs or tooling.
|
||||
|
||||
When those conditions are met, this step is mandatory. If image generation isn't natively available, skip silently and proceed; don't announce the skip.
|
||||
When those conditions are met, this step is mandatory. If image generation isn't natively available, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed. The one-line announcement is required, not optional; it forces a conscious decision instead of letting the step quietly evaporate.
|
||||
|
||||
Use probes to explore visual lanes, not to replace the brief.
|
||||
|
||||
@@ -107,7 +107,7 @@ The probes should differ in primary visual direction (hierarchy, topology, densi
|
||||
- Do **not** treat generated imagery as final UX specification, final copy, or final accessibility behavior.
|
||||
- Do **not** use this step for minor refinements of existing work. It's for shaping a new surface or clarifying a big directional choice.
|
||||
|
||||
If image generation isn't natively available, skip this phase silently and proceed.
|
||||
If image generation isn't natively available, announce the skip in one line and proceed to the design brief.
|
||||
|
||||
## Phase 2: Design Brief
|
||||
|
||||
@@ -120,7 +120,7 @@ After the interview and any required probes, present a brief and **end your resp
|
||||
|
||||
Don't pad a clear brief into a long one to look thorough. A 70-line brief restating answers the user just gave is noise, not rigor. Equally, don't skip the confirmation pause to look efficient: the pause is the point.
|
||||
|
||||
If the user already said "approved" or "go" during discovery for the *exact direction you'd present*, that counts as confirmation; you may proceed without asking again. But if your brief adds anything the user hasn't seen and approved, you must stop and confirm.
|
||||
Present the brief, then **stop and wait for explicit confirmation**. You are not the judge of whether the user already approved. Even when the brief feels obviously right, ask once and wait. The pause is what separates shape from premature implementation.
|
||||
|
||||
### Brief Structure
|
||||
|
||||
@@ -158,8 +158,8 @@ Anything genuinely unresolved. Don't list "open questions" you've already recomm
|
||||
|
||||
---
|
||||
|
||||
ask the user directly to clarify what you cannot infer.
|
||||
ask the user directly to clarify what you cannot infer. Ask for explicit confirmation of the brief before finishing.
|
||||
|
||||
If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the user confirms direction (or the user already gave a clear go during discovery, which counts).
|
||||
If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the user confirms direction.
|
||||
|
||||
Once confirmed, the brief is complete. The user can now hand it to /impeccable, or use it to guide any other implementation approach. (If the user wants the full discovery-then-build flow in one step, they should use /impeccable craft instead, which runs this command internally.)
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Critique persistence helper.
|
||||
*
|
||||
* Each run of /impeccable critique writes a per-target snapshot to
|
||||
* .impeccable/critique/<timestamp>__<slug>.md
|
||||
* with a small YAML frontmatter carrying the score + P0/P1 counts.
|
||||
*
|
||||
* /impeccable polish reads the latest matching snapshot at start as its
|
||||
* fix backlog. No other skill auto-reads critique output.
|
||||
*
|
||||
* The slug is derived mechanically from the *resolved* primary artifact
|
||||
* (file path or URL), never from the user's natural-language phrasing.
|
||||
* Slug stability across runs is what lets the trend display work.
|
||||
*
|
||||
* CLI entry points (called from skill instructions):
|
||||
* node critique-storage.mjs slug <resolved-target>
|
||||
* node critique-storage.mjs write <slug> <snapshot-body-file>
|
||||
* node critique-storage.mjs latest <slug>
|
||||
* node critique-storage.mjs trend <slug> [limit]
|
||||
*
|
||||
* Note: there is intentionally no `ignore` subcommand. ignore.md is a plain
|
||||
* markdown file; the model reads it directly with its file-read tool. This
|
||||
* helper only exists for operations the model can't trivially do inline
|
||||
* (normalizing paths, generating filenames, globbing + parsing frontmatter).
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { getCritiqueDir } from './impeccable-paths.mjs';
|
||||
|
||||
const SLUG_MAX = 50;
|
||||
|
||||
/**
|
||||
* Mechanically derive a slug from a resolved target. Returns null if the
|
||||
* input doesn't look like a stable identifier (empty, project root, etc).
|
||||
*
|
||||
* Accepts file paths and URLs. The model resolves "the homepage" to a
|
||||
* concrete artifact before calling this — we never slug a natural-language
|
||||
* phrase.
|
||||
*/
|
||||
export function slugFromTarget(resolved, { cwd = process.cwd() } = {}) {
|
||||
if (!resolved || typeof resolved !== 'string') return null;
|
||||
const trimmed = resolved.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
// URL
|
||||
if (/^https?:\/\//i.test(trimmed)) {
|
||||
let url;
|
||||
try { url = new URL(trimmed); } catch { return null; }
|
||||
const hostPath = `${url.hostname}${url.pathname}`;
|
||||
return kebab(hostPath);
|
||||
}
|
||||
|
||||
// File path. Make it project-relative so two devs critiquing the same
|
||||
// checkout get the same slug regardless of where their repo is cloned.
|
||||
const abs = path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
|
||||
let rel = path.relative(cwd, abs);
|
||||
// If the target is outside cwd, fall back to the basename so we still
|
||||
// produce a stable slug (vs the absolute path, which would include
|
||||
// home dirs / usernames).
|
||||
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
||||
rel = path.basename(abs);
|
||||
}
|
||||
if (!rel || rel === '.' || rel === '') return null;
|
||||
return kebab(rel);
|
||||
}
|
||||
|
||||
function kebab(s) {
|
||||
const slug = s
|
||||
.toLowerCase()
|
||||
.replace(/[/\\.]+/g, '-')
|
||||
.replace(/[^a-z0-9-]+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
if (!slug) return null;
|
||||
// Cap from the tail — the tail (filename) is more identifying than the
|
||||
// top-level directory.
|
||||
return slug.length <= SLUG_MAX ? slug : slug.slice(slug.length - SLUG_MAX).replace(/^-/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filename-safe UTC ISO timestamp: hyphens for separators, trailing Z.
|
||||
* Plain colons aren't allowed on Windows filesystems.
|
||||
*/
|
||||
export function nowFilenameStamp(date = new Date()) {
|
||||
const iso = date.toISOString(); // 2026-05-12T18:30:00.123Z
|
||||
return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z');
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a snapshot for `slug`. `meta` carries the small structured frontmatter
|
||||
* keys read back by readTrend(). `body` is the human-readable critique
|
||||
* report (everything below the frontmatter).
|
||||
*
|
||||
* Returns the absolute path written.
|
||||
*/
|
||||
export function writeSnapshot({ slug, meta, body, cwd = process.cwd(), now = new Date() }) {
|
||||
if (!slug) throw new Error('writeSnapshot requires a slug');
|
||||
const dir = getCritiqueDir(cwd);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const timestamp = nowFilenameStamp(now);
|
||||
const filePath = path.join(dir, `${timestamp}__${slug}.md`);
|
||||
// Spread `meta` first so internally computed `timestamp` and `slug`
|
||||
// always win. Otherwise a caller-supplied meta blob (parsed from the
|
||||
// IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the
|
||||
// filename in disagreement with its frontmatter and corrupting trends.
|
||||
const front = serializeFrontmatter({ ...meta, timestamp, slug });
|
||||
fs.writeFileSync(filePath, `${front}\n${body.trim()}\n`, 'utf-8');
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function serializeFrontmatter(obj) {
|
||||
const lines = ['---'];
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
const str = typeof value === 'string' ? value : String(value);
|
||||
// Quote strings that contain : or # to keep parsing simple.
|
||||
const needsQuotes = typeof value === 'string' && /[:#]/.test(str);
|
||||
lines.push(`${key}: ${needsQuotes ? JSON.stringify(str) : str}`);
|
||||
}
|
||||
lines.push('---');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function parseFrontmatter(text) {
|
||||
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
if (!match) return {};
|
||||
const out = {};
|
||||
for (const line of match[1].split(/\r?\n/)) {
|
||||
const colon = line.indexOf(':');
|
||||
if (colon < 0) continue;
|
||||
const key = line.slice(0, colon).trim();
|
||||
let value = line.slice(colon + 1).trim();
|
||||
if (/^".*"$/.test(value)) {
|
||||
try { value = JSON.parse(value); } catch { /* leave as-is */ }
|
||||
} else if (/^-?\d+$/.test(value)) {
|
||||
value = Number(value);
|
||||
}
|
||||
out[key] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all snapshot files for `slug`, sorted oldest → newest.
|
||||
*/
|
||||
function listSnapshotsForSlug(slug, cwd) {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const suffix = `__${slug}.md`;
|
||||
return fs.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(suffix))
|
||||
.sort()
|
||||
.map((f) => path.join(dir, f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent snapshot for `slug`, or null. Polish reads this
|
||||
* to find its fix backlog when the slug matches.
|
||||
*/
|
||||
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
if (!all.length) return null;
|
||||
const latest = all[all.length - 1];
|
||||
const body = fs.readFileSync(latest, 'utf-8');
|
||||
return { path: latest, body, meta: parseFrontmatter(body) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last `limit` snapshots' frontmatter, oldest → newest.
|
||||
* Critique appends a one-line trend to its output using this.
|
||||
*/
|
||||
export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
const slice = all.slice(-limit);
|
||||
return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8')));
|
||||
}
|
||||
|
||||
// ---- CLI ---------------------------------------------------------------
|
||||
|
||||
function main(argv) {
|
||||
const [cmd, ...args] = argv;
|
||||
switch (cmd) {
|
||||
case 'slug': {
|
||||
const slug = slugFromTarget(args[0]);
|
||||
if (!slug) { process.stderr.write('no stable slug for input\n'); process.exit(1); }
|
||||
process.stdout.write(`${slug}\n`);
|
||||
return;
|
||||
}
|
||||
case 'write': {
|
||||
const [slug, bodyFile] = args;
|
||||
if (!slug || !bodyFile) { process.stderr.write('usage: write <slug> <body-file>\n'); process.exit(1); }
|
||||
const raw = fs.readFileSync(bodyFile, 'utf-8');
|
||||
// The body file may be a full report. The caller passes the meta as
|
||||
// a JSON object on stdin if it wants structured frontmatter; otherwise
|
||||
// we write with minimal metadata.
|
||||
let meta = {};
|
||||
const metaArg = process.env.IMPECCABLE_CRITIQUE_META;
|
||||
if (metaArg) {
|
||||
try { meta = JSON.parse(metaArg); } catch { /* ignore */ }
|
||||
}
|
||||
const out = writeSnapshot({ slug, meta, body: raw });
|
||||
process.stdout.write(`${out}\n`);
|
||||
return;
|
||||
}
|
||||
case 'latest': {
|
||||
const latest = readLatestSnapshot(args[0]);
|
||||
if (!latest) { process.exit(2); }
|
||||
process.stdout.write(latest.body);
|
||||
return;
|
||||
}
|
||||
case 'trend': {
|
||||
const rows = readTrend(args[0], { limit: args[1] ? Number(args[1]) : 5 });
|
||||
process.stdout.write(JSON.stringify(rows, null, 2) + '\n');
|
||||
return;
|
||||
}
|
||||
default:
|
||||
process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend> [args]\n');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main(process.argv.slice(2));
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import path from 'node:path';
|
||||
|
||||
export const IMPECCABLE_DIR = '.impeccable';
|
||||
export const LIVE_DIR = 'live';
|
||||
export const CRITIQUE_DIR = 'critique';
|
||||
|
||||
export function getImpeccableDir(cwd = process.cwd()) {
|
||||
return path.join(cwd, IMPECCABLE_DIR);
|
||||
@@ -96,6 +97,10 @@ export function getLiveAnnotationsDir(cwd = process.cwd()) {
|
||||
return path.join(getLiveDir(cwd), 'annotations');
|
||||
}
|
||||
|
||||
export function getCritiqueDir(cwd = process.cwd()) {
|
||||
return path.join(getImpeccableDir(cwd), CRITIQUE_DIR);
|
||||
}
|
||||
|
||||
export function getLegacyLiveAnnotationsDir(cwd = process.cwd()) {
|
||||
return path.join(cwd, '.impeccable-live', 'annotations');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 3.0.7
|
||||
version: 3.1.0
|
||||
user-invocable: true
|
||||
argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · teach|document|extract|live] [target]"
|
||||
license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution.
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# Codex: Visual Direction & Asset Production
|
||||
|
||||
This file is loaded by `/impeccable craft` when the harness has native image generation (currently Codex via `image_gen`). Other harnesses skip it. It covers the two craft steps that depend on real image generation: landing the visual direction, and producing the raster assets the implementation will compose.
|
||||
|
||||
Read this *before* generating any images. The order matters, and the per-step user pauses are what keep generated imagery from drifting away from the brief.
|
||||
|
||||
## Step A: Explore Directions with the User
|
||||
|
||||
Before generating anything, run a brief direction conversation grounded in the shape brief.
|
||||
|
||||
Ask **2-3 targeted questions** about visual lane, color strategy, atmosphere, and named anchor references. Don't enumerate generic menus; tie each question to the shape brief's answers. Example shape-grounded questions:
|
||||
|
||||
- "Brief says 'editorial restraint, Klim-adjacent.' Are we closer to a quiet specimen page or a magazine-spread feel with hero imagery?"
|
||||
- "Palette strategy from shape was 'Committed.' Want it warm-grounded (deep oxblood + cream) or cool-grounded (slate + paper white)?"
|
||||
|
||||
Stop and wait for answers. These pin the palette before any pixel gets generated.
|
||||
|
||||
## Step B: Generate the Brand Palette First
|
||||
|
||||
Generate **one** palette artifact before any mocks. This is a small, focused image: typography pairing on the chosen background, primary + accent color swatches, one signature ornament or motif. Single image, single pass.
|
||||
|
||||
Why palette first: mocks generated against a vague color sense produce noise that drowns out the structural decisions. A confirmed palette is the first concrete contract for everything downstream.
|
||||
|
||||
Show the palette to the user. Ask one question: "This is the palette I'm locking in for the mocks. Confirm, or call out what to shift?"
|
||||
|
||||
Wait for confirmation. Do not generate mocks against an unconfirmed palette.
|
||||
|
||||
## Step C: Generate 1-3 Visual Mocks Against the Palette
|
||||
|
||||
Once the palette is confirmed, generate **1 to 3** high-fidelity north-star comps. Each mock must use the confirmed palette and typography. Mocks differ in *structural* direction (hierarchy, topology, density, composition), not in color or motif.
|
||||
|
||||
- Brand work: push visual identity, composition, mood, and signature motifs.
|
||||
- Product work: push hierarchy, topology, density, tone, grounded in realistic product structure.
|
||||
- Landing pages and long-form brand surfaces: show enough of the second fold to establish the system beyond the hero.
|
||||
|
||||
Use the `image_gen` tool directly (or via the imagegen skill when available). Don't ask the user to install anything.
|
||||
|
||||
## Step D: Approval Loop
|
||||
|
||||
Show the comps. Ask what carries forward. Iterate until **one direction is approved** or the user explicitly delegates.
|
||||
|
||||
If the user delegates, pick the strongest direction and explain it from the brief, not personal taste.
|
||||
|
||||
Before moving to assets, summarize what to carry into code and what *not* to literalize from the mock. This is the handoff between visual exploration and semantic implementation.
|
||||
|
||||
## Step E: Mock Fidelity Inventory
|
||||
|
||||
Inventory the approved mock's major visible ingredients. For each, decide implementation: semantic HTML/CSS/SVG, generated raster, sourced raster, icon library, canvas/WebGL, or accepted omission.
|
||||
|
||||
Common ingredients to inventory:
|
||||
|
||||
- Hero silhouette and dominant composition
|
||||
- Signature motifs (planets, devices, portraits, charts, route lines, insets, badges, etc.)
|
||||
- Nav and primary CTA treatment
|
||||
- Section sequence, especially the second fold
|
||||
- Image-native content the concept depends on
|
||||
- Typography, density, color/material treatment, motion cues
|
||||
|
||||
Treat the mock as a north star, not a screenshot to trace. Don't rasterize core UI text. But if the live result lacks the mock's major ingredients, the implementation is wrong.
|
||||
|
||||
If a photographic, architectural, product, or place-led mock becomes generic CSS scenery, decorative diagrams, bullets, or copy, stop and fix it. That's a broken implementation, not a harmless interpretation.
|
||||
|
||||
Don't substitute a different hero composition or visual driver post-approval without user sign-off.
|
||||
|
||||
## Step F: Asset Slicing via the Asset Producer
|
||||
|
||||
Raster ingredients identified in Step E need clean production assets. Use the bundled `impeccable_asset_producer` subagent rather than producing inline.
|
||||
|
||||
Spawn it as a scoped subagent. If you do not have explicit permission to use agents, stop and ask:
|
||||
|
||||
```text
|
||||
Asset production will work better as a scoped subagent job. Should I spawn the Impeccable asset producer subagent for this step?
|
||||
```
|
||||
|
||||
Pass to the agent:
|
||||
|
||||
- Approved mock path or screenshot reference
|
||||
- Crop paths or a contact sheet with crop ids
|
||||
- Output directory
|
||||
- Required dimensions, format, transparency needs
|
||||
- Avoid list
|
||||
- Notes on what should remain semantic HTML/CSS/SVG instead of raster
|
||||
|
||||
Attach image generation capability to the spawned agent when the harness supports it. Do **not** load image-generation reference material into the parent thread.
|
||||
|
||||
Inline asset production is allowed only if the user declines subagents, the harness cannot spawn the authorized agent, or the user explicitly asks for single-thread mode.
|
||||
|
||||
Prefer HTML/CSS/SVG/canvas when they can credibly reproduce an ingredient; reach for real, generated, or stock imagery when the mock or subject matter calls for actual visual content.
|
||||
|
||||
## After This File
|
||||
|
||||
Once Steps A through F are complete, return to `craft.md` Step 5 (Build to Production Quality). The implementation builds against the confirmed palette, approved mock, and the assets the producer wrote.
|
||||
@@ -51,64 +51,15 @@ Then add references based on the brief's needs:
|
||||
- Responsive requirements? Consult [responsive-design.md](responsive-design.md)
|
||||
- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.md)
|
||||
|
||||
## Step 3: Land the Visual Direction (Capability-Gated)
|
||||
## Step 3: Visual Direction & Assets (Harness-Gated)
|
||||
|
||||
Generate high-fidelity visual comps before implementation when all three are true:
|
||||
If the harness has **native image generation** (currently Codex via `image_gen`), this step is mandatory. **Stop and load [codex.md](codex.md)**. It covers palette generation, mock exploration, the approval loop, mock-fidelity inventory, and asset slicing via the `impeccable_asset_producer` subagent. Follow Steps A-F in that file, then return here for Step 4.
|
||||
|
||||
- The work is net-new or visually open-ended enough that composition exploration will improve the build.
|
||||
- The brief's scope is mid-fi, high-fi, or production-ready.
|
||||
- The harness gives you native image generation (Codex's `image_gen`, an equivalent MCP tool). Don't ask the user to set up external APIs.
|
||||
If the harness lacks native image generation, **state in one line that the visual-direction-by-generation step is being skipped because the harness lacks native image generation, then proceed**. The one-line announcement is required; it forces a conscious decision instead of letting the step quietly evaporate. The brief is your only visual reference. Implement directly from it, treating any named anchor references and the brief's "Design Direction" as the contract.
|
||||
|
||||
When met, this step is mandatory for both brand and product work. If image generation isn't available, skip silently. "The eventual UI is semantic/code-native/accessible" is not a reason to skip; those are implementation requirements, not exploration ones.
|
||||
Whether you generated mocks or not: don't replace required imagery with generic cards, bullets, emoji, fake metrics, decorative CSS panels, or filler copy. Image-led briefs (restaurants, hotels, magazines, photography, hobbyist communities, food, travel, fashion, product) need real or sourced imagery in the build, not CSS scenery.
|
||||
|
||||
### What to generate
|
||||
|
||||
Generate **1 to 3** high-fidelity north-star comps from the confirmed brief. If shape already produced direction probes, resolve the winning lane further, not an unrelated exploration. Comps must differ in primary visual direction, not just color.
|
||||
|
||||
- Brand work: push visual identity, composition, and mood aggressively.
|
||||
- Product work: push hierarchy, topology, density, tone, grounded in realistic product structure.
|
||||
- Landing pages and long-form brand surfaces: show enough of the second fold to establish the system beyond the hero.
|
||||
|
||||
### Approval loop
|
||||
|
||||
Show the comps and ask what carries forward. Iterate until one direction is approved or the user delegates. If the user delegates, pick the strongest direction and explain it from the brief, not personal taste.
|
||||
|
||||
Before Step 4, summarize what to carry into code and what **not** to literalize from the mock. This is the handoff between visual exploration and semantic implementation.
|
||||
|
||||
### Mock fidelity inventory
|
||||
|
||||
Inventory the approved mock's major visible ingredients:
|
||||
|
||||
- Hero silhouette and dominant composition
|
||||
- Signature motifs (planets, devices, portraits, charts, route lines, insets, badges, etc.)
|
||||
- Nav and primary CTA treatment
|
||||
- Section sequence, especially the second fold
|
||||
- Image-native content the concept depends on
|
||||
- Typography, density, color/material treatment, motion cues
|
||||
|
||||
For each, decide implementation: semantic HTML/CSS/SVG, generated asset, sourced asset, icon library, canvas/WebGL, or accepted omission. Don't substitute a different hero composition or visual driver post-approval without user sign-off.
|
||||
|
||||
If a photographic, architectural, product, or place-led mock becomes generic CSS scenery / decorative diagrams / bullets / copy, stop and fix it. That's a broken implementation, not a harmless interpretation.
|
||||
|
||||
Treat the mock as a north star, not a screenshot to trace. Don't rasterize core UI text. But if the live result lacks the mock's major ingredients, the implementation is wrong.
|
||||
|
||||
## Step 4: Asset Extraction (Need-Gated)
|
||||
|
||||
If the approved direction needs raster assets, create them before building. Do not replace required imagery with generic cards, bullets, emoji, fake metrics, decorative CSS panels, or filler copy.
|
||||
|
||||
Use the native asset producer (`impeccable_asset_producer` in Codex, `impeccable-asset-producer` in Claude Code) to create clean assets from the hi-fi mock and crops. If you do not have explicit permission to use agents, stop and ask:
|
||||
|
||||
```text
|
||||
Asset production will work better as a scoped subagent job. Should I spawn the Impeccable asset producer subagent for this step?
|
||||
```
|
||||
|
||||
Do not skip asset production or silently do it inline. Inline asset production is allowed only if the user declines subagents, the harness cannot spawn the authorized agent, or the user explicitly asks for single-thread mode.
|
||||
|
||||
Pass the approved mock, crop/contact-sheet paths, output directory, dimensions/formats, transparency needs, constraints, and avoid list to the asset producer. Attach image generation capability to the spawned agent when the harness supports it; do not load image-generation reference material into the parent thread first.
|
||||
|
||||
Prefer HTML/CSS/SVG/canvas when they can credibly reproduce an ingredient; use real/generated/stock imagery when the mock or subject matter calls for actual visual content.
|
||||
|
||||
## Step 5: Build to Production Quality
|
||||
## Step 4: Build to Production Quality
|
||||
|
||||
Implement the feature following the design brief. Build in passes so structure, visual system, states, motion/media, and responsive behavior each get deliberate attention. The list below is the definition of done, not inspiration.
|
||||
|
||||
@@ -130,7 +81,7 @@ Implement the feature following the design brief. Build in passes so structure,
|
||||
- **Technically clean.** Production build passes, no console errors, no avoidable layout shift, no needless dependencies, no broken asset paths.
|
||||
- **Ask when uncertain.** If a discovery materially changes the brief or approved direction, stop and ask. Don't guess.
|
||||
|
||||
## Step 6: Iterate Visually
|
||||
## Step 5: Iterate Visually
|
||||
|
||||
Look at what you built like a designer would. Your eyes are whatever the harness gives you: a connected browser, a screenshotting tool, Playwright, or asking the user. Use them for responsive testing (mobile, tablet, desktop minimum) and general visual validation.
|
||||
|
||||
@@ -144,7 +95,7 @@ Actively check: responsive behavior (composes, not shrinks), every state (empty
|
||||
|
||||
Detector or QA output is defect evidence only; never proof the work is finished.
|
||||
|
||||
## Step 7: Present
|
||||
## Step 6: Present
|
||||
|
||||
Present the result to the user:
|
||||
- Show the feature in its primary state
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
> **Additional context needed**: what the interface is trying to accomplish.
|
||||
|
||||
### Setup: Resolve Target and Load Ignore List
|
||||
|
||||
Before gathering assessments, do two small bookkeeping steps. They cost almost nothing and they're what makes critique iterative across runs.
|
||||
|
||||
1. **Resolve the primary artifact.** The user's phrasing ("the homepage", "the pricing flow") is not stable enough to track across runs. Resolve it to a concrete file path or URL: the same one you'd already need to scan code or open in a browser. Examples:
|
||||
- "the homepage" → `site/pages/index.astro` (or `http://localhost:3000/` if you're inspecting live)
|
||||
- "the settings modal" → the primary component file (e.g., `src/components/Settings.tsx`)
|
||||
- "this page" → the URL or the page's source file
|
||||
Prefer the source file path over the dev-server URL when both exist; ports drift between runs (`bun dev` vs `bun preview`), file paths don't.
|
||||
|
||||
2. **Compute the slug.** Run:
|
||||
```bash
|
||||
node .opencode/skills/impeccable/scripts/critique-storage.mjs slug "<resolved-path-or-url>"
|
||||
```
|
||||
Keep the printed slug. It identifies this target's stream across runs. If the command exits non-zero ("no stable slug for input"), skip persistence for this run and tell the user; the trend won't update but the critique still goes ahead.
|
||||
|
||||
3. **Read the ignore list** at `.impeccable/critique/ignore.md` if it exists. Plain markdown; each non-empty, non-comment line is something the user has marked as "do not re-raise" (deferred tradeoffs, designer-intended deviations, detector false-positives the user accepts). When a finding's text matches a line here (case-insensitive substring against rule name or snippet), **drop it silently**. Do not mention it in the report. This is the ONLY input critique consumes from prior runs; anchoring on prior findings would defeat the point of independent assessment.
|
||||
|
||||
### Gather Assessments
|
||||
|
||||
Launch two independent assessments. **Neither may see the other's output.** This isolation is what makes the combined score honest. Running both in one head silently anchors them to each other; do not shortcut it for cost, speed, or context-size reasons.
|
||||
@@ -164,6 +182,36 @@ Provocative questions that might unlock better solutions:
|
||||
- Prioritize ruthlessly. If everything is important, nothing is.
|
||||
- Don't soften criticism. Developers need honest feedback to ship great design.
|
||||
|
||||
### Persist the Snapshot
|
||||
|
||||
Once the report above is finalized, write it to `.impeccable/critique/` so the user can refer back, and so `/impeccable polish` can pick up the priority issues without a copy-paste.
|
||||
|
||||
Skip this step if the Setup slug was null (vague or root-level target).
|
||||
|
||||
1. **Write the body to a temp file** so you can pipe it to the helper. Use the full report (heuristic table, anti-patterns verdict, priority issues, persona red flags) but stop before the "Ask the User" / "Recommended Actions" sections that come later.
|
||||
|
||||
2. **Pass the structured metadata** through `IMPECCABLE_CRITIQUE_META` (JSON), then run the write command:
|
||||
```bash
|
||||
IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"p0_count":<n>,"p1_count":<n>}' \
|
||||
node .opencode/skills/impeccable/scripts/critique-storage.mjs write <slug> <body-file>
|
||||
```
|
||||
The helper prints the absolute path it wrote.
|
||||
|
||||
3. **Read the trend** for context:
|
||||
```bash
|
||||
node .opencode/skills/impeccable/scripts/critique-storage.mjs trend <slug> 5
|
||||
```
|
||||
This returns a JSON array of the last 5 frontmatter entries (including the one you just wrote).
|
||||
|
||||
4. **Append a single line to the user-visible output**, after the report and before the questions:
|
||||
|
||||
> **Trend for `<slug>` (last 5 runs): 24 → 28 → 32 → 29 → 32**
|
||||
> Wrote `.impeccable/critique/<filename>`.
|
||||
|
||||
If this is the first run for the slug, the trend is just one score; say so: "First run for this target, no trend yet."
|
||||
|
||||
This is fire-and-forget. Do not show the user the helper's JSON output; only the human-readable trend line and the written path. Failures here should not block the rest of the flow; print the error and move on.
|
||||
|
||||
### Ask the User
|
||||
|
||||
**After presenting findings**, use targeted questions based on what was actually found. STOP and call the `question` tool to clarify. These answers will shape the action plan.
|
||||
|
||||
@@ -35,7 +35,14 @@ Understand the current state and goals before touching anything:
|
||||
- Loading and transition smoothness
|
||||
- Information architecture and flow drift (does this feature reveal complexity the way neighboring features do?)
|
||||
|
||||
4. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent; never perfect one corner while leaving another rough.
|
||||
4. **Pull in any prior critique** (optional signal): If `/impeccable critique` has been run on the same target, its priority issues are a useful prior for what to address first. Resolve the target to a file path or URL, then:
|
||||
```bash
|
||||
slug=$(node .opencode/skills/impeccable/scripts/critique-storage.mjs slug "<resolved>")
|
||||
node .opencode/skills/impeccable/scripts/critique-storage.mjs latest "$slug"
|
||||
```
|
||||
Exit 0 with body = found; fold the P0/P1 items into your polish list and mention the snapshot path so the user sees what you read. Exit 2 = no snapshot, continue without it. The critique is one input among many. Do your own pass either way.
|
||||
|
||||
5. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent; never perfect one corner while leaving another rough.
|
||||
|
||||
**CRITICAL**: Polish is the last step, not the first. Don't polish work that's not functionally complete.
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ After the discovery interview, generate a small set of visual direction probes *
|
||||
- The requested fidelity is **mid-fi, high-fi, or production-ready**. Skip for sketch-only planning.
|
||||
- The current harness gives you native image generation (Codex's `image_gen`, an equivalent MCP tool, or similar). Don't ask the user to install APIs or tooling.
|
||||
|
||||
When those conditions are met, this step is mandatory. If image generation isn't natively available, skip silently and proceed; don't announce the skip.
|
||||
When those conditions are met, this step is mandatory. If image generation isn't natively available, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed. The one-line announcement is required, not optional; it forces a conscious decision instead of letting the step quietly evaporate.
|
||||
|
||||
Use probes to explore visual lanes, not to replace the brief.
|
||||
|
||||
@@ -107,7 +107,7 @@ The probes should differ in primary visual direction (hierarchy, topology, densi
|
||||
- Do **not** treat generated imagery as final UX specification, final copy, or final accessibility behavior.
|
||||
- Do **not** use this step for minor refinements of existing work. It's for shaping a new surface or clarifying a big directional choice.
|
||||
|
||||
If image generation isn't natively available, skip this phase silently and proceed.
|
||||
If image generation isn't natively available, announce the skip in one line and proceed to the design brief.
|
||||
|
||||
## Phase 2: Design Brief
|
||||
|
||||
@@ -120,7 +120,7 @@ After the interview and any required probes, present a brief and **end your resp
|
||||
|
||||
Don't pad a clear brief into a long one to look thorough. A 70-line brief restating answers the user just gave is noise, not rigor. Equally, don't skip the confirmation pause to look efficient: the pause is the point.
|
||||
|
||||
If the user already said "approved" or "go" during discovery for the *exact direction you'd present*, that counts as confirmation; you may proceed without asking again. But if your brief adds anything the user hasn't seen and approved, you must stop and confirm.
|
||||
Present the brief, then **stop and wait for explicit confirmation**. You are not the judge of whether the user already approved. Even when the brief feels obviously right, ask once and wait. The pause is what separates shape from premature implementation.
|
||||
|
||||
### Brief Structure
|
||||
|
||||
@@ -158,8 +158,8 @@ Anything genuinely unresolved. Don't list "open questions" you've already recomm
|
||||
|
||||
---
|
||||
|
||||
STOP and call the `question` tool to clarify.
|
||||
STOP and call the `question` tool to clarify. Ask for explicit confirmation of the brief before finishing.
|
||||
|
||||
If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the user confirms direction (or the user already gave a clear go during discovery, which counts).
|
||||
If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the user confirms direction.
|
||||
|
||||
Once confirmed, the brief is complete. The user can now hand it to /impeccable, or use it to guide any other implementation approach. (If the user wants the full discovery-then-build flow in one step, they should use /impeccable craft instead, which runs this command internally.)
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Critique persistence helper.
|
||||
*
|
||||
* Each run of /impeccable critique writes a per-target snapshot to
|
||||
* .impeccable/critique/<timestamp>__<slug>.md
|
||||
* with a small YAML frontmatter carrying the score + P0/P1 counts.
|
||||
*
|
||||
* /impeccable polish reads the latest matching snapshot at start as its
|
||||
* fix backlog. No other skill auto-reads critique output.
|
||||
*
|
||||
* The slug is derived mechanically from the *resolved* primary artifact
|
||||
* (file path or URL), never from the user's natural-language phrasing.
|
||||
* Slug stability across runs is what lets the trend display work.
|
||||
*
|
||||
* CLI entry points (called from skill instructions):
|
||||
* node critique-storage.mjs slug <resolved-target>
|
||||
* node critique-storage.mjs write <slug> <snapshot-body-file>
|
||||
* node critique-storage.mjs latest <slug>
|
||||
* node critique-storage.mjs trend <slug> [limit]
|
||||
*
|
||||
* Note: there is intentionally no `ignore` subcommand. ignore.md is a plain
|
||||
* markdown file; the model reads it directly with its file-read tool. This
|
||||
* helper only exists for operations the model can't trivially do inline
|
||||
* (normalizing paths, generating filenames, globbing + parsing frontmatter).
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { getCritiqueDir } from './impeccable-paths.mjs';
|
||||
|
||||
const SLUG_MAX = 50;
|
||||
|
||||
/**
|
||||
* Mechanically derive a slug from a resolved target. Returns null if the
|
||||
* input doesn't look like a stable identifier (empty, project root, etc).
|
||||
*
|
||||
* Accepts file paths and URLs. The model resolves "the homepage" to a
|
||||
* concrete artifact before calling this — we never slug a natural-language
|
||||
* phrase.
|
||||
*/
|
||||
export function slugFromTarget(resolved, { cwd = process.cwd() } = {}) {
|
||||
if (!resolved || typeof resolved !== 'string') return null;
|
||||
const trimmed = resolved.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
// URL
|
||||
if (/^https?:\/\//i.test(trimmed)) {
|
||||
let url;
|
||||
try { url = new URL(trimmed); } catch { return null; }
|
||||
const hostPath = `${url.hostname}${url.pathname}`;
|
||||
return kebab(hostPath);
|
||||
}
|
||||
|
||||
// File path. Make it project-relative so two devs critiquing the same
|
||||
// checkout get the same slug regardless of where their repo is cloned.
|
||||
const abs = path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
|
||||
let rel = path.relative(cwd, abs);
|
||||
// If the target is outside cwd, fall back to the basename so we still
|
||||
// produce a stable slug (vs the absolute path, which would include
|
||||
// home dirs / usernames).
|
||||
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
||||
rel = path.basename(abs);
|
||||
}
|
||||
if (!rel || rel === '.' || rel === '') return null;
|
||||
return kebab(rel);
|
||||
}
|
||||
|
||||
function kebab(s) {
|
||||
const slug = s
|
||||
.toLowerCase()
|
||||
.replace(/[/\\.]+/g, '-')
|
||||
.replace(/[^a-z0-9-]+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
if (!slug) return null;
|
||||
// Cap from the tail — the tail (filename) is more identifying than the
|
||||
// top-level directory.
|
||||
return slug.length <= SLUG_MAX ? slug : slug.slice(slug.length - SLUG_MAX).replace(/^-/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filename-safe UTC ISO timestamp: hyphens for separators, trailing Z.
|
||||
* Plain colons aren't allowed on Windows filesystems.
|
||||
*/
|
||||
export function nowFilenameStamp(date = new Date()) {
|
||||
const iso = date.toISOString(); // 2026-05-12T18:30:00.123Z
|
||||
return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z');
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a snapshot for `slug`. `meta` carries the small structured frontmatter
|
||||
* keys read back by readTrend(). `body` is the human-readable critique
|
||||
* report (everything below the frontmatter).
|
||||
*
|
||||
* Returns the absolute path written.
|
||||
*/
|
||||
export function writeSnapshot({ slug, meta, body, cwd = process.cwd(), now = new Date() }) {
|
||||
if (!slug) throw new Error('writeSnapshot requires a slug');
|
||||
const dir = getCritiqueDir(cwd);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const timestamp = nowFilenameStamp(now);
|
||||
const filePath = path.join(dir, `${timestamp}__${slug}.md`);
|
||||
// Spread `meta` first so internally computed `timestamp` and `slug`
|
||||
// always win. Otherwise a caller-supplied meta blob (parsed from the
|
||||
// IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the
|
||||
// filename in disagreement with its frontmatter and corrupting trends.
|
||||
const front = serializeFrontmatter({ ...meta, timestamp, slug });
|
||||
fs.writeFileSync(filePath, `${front}\n${body.trim()}\n`, 'utf-8');
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function serializeFrontmatter(obj) {
|
||||
const lines = ['---'];
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
const str = typeof value === 'string' ? value : String(value);
|
||||
// Quote strings that contain : or # to keep parsing simple.
|
||||
const needsQuotes = typeof value === 'string' && /[:#]/.test(str);
|
||||
lines.push(`${key}: ${needsQuotes ? JSON.stringify(str) : str}`);
|
||||
}
|
||||
lines.push('---');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function parseFrontmatter(text) {
|
||||
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
if (!match) return {};
|
||||
const out = {};
|
||||
for (const line of match[1].split(/\r?\n/)) {
|
||||
const colon = line.indexOf(':');
|
||||
if (colon < 0) continue;
|
||||
const key = line.slice(0, colon).trim();
|
||||
let value = line.slice(colon + 1).trim();
|
||||
if (/^".*"$/.test(value)) {
|
||||
try { value = JSON.parse(value); } catch { /* leave as-is */ }
|
||||
} else if (/^-?\d+$/.test(value)) {
|
||||
value = Number(value);
|
||||
}
|
||||
out[key] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all snapshot files for `slug`, sorted oldest → newest.
|
||||
*/
|
||||
function listSnapshotsForSlug(slug, cwd) {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const suffix = `__${slug}.md`;
|
||||
return fs.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(suffix))
|
||||
.sort()
|
||||
.map((f) => path.join(dir, f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent snapshot for `slug`, or null. Polish reads this
|
||||
* to find its fix backlog when the slug matches.
|
||||
*/
|
||||
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
if (!all.length) return null;
|
||||
const latest = all[all.length - 1];
|
||||
const body = fs.readFileSync(latest, 'utf-8');
|
||||
return { path: latest, body, meta: parseFrontmatter(body) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last `limit` snapshots' frontmatter, oldest → newest.
|
||||
* Critique appends a one-line trend to its output using this.
|
||||
*/
|
||||
export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
const slice = all.slice(-limit);
|
||||
return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8')));
|
||||
}
|
||||
|
||||
// ---- CLI ---------------------------------------------------------------
|
||||
|
||||
function main(argv) {
|
||||
const [cmd, ...args] = argv;
|
||||
switch (cmd) {
|
||||
case 'slug': {
|
||||
const slug = slugFromTarget(args[0]);
|
||||
if (!slug) { process.stderr.write('no stable slug for input\n'); process.exit(1); }
|
||||
process.stdout.write(`${slug}\n`);
|
||||
return;
|
||||
}
|
||||
case 'write': {
|
||||
const [slug, bodyFile] = args;
|
||||
if (!slug || !bodyFile) { process.stderr.write('usage: write <slug> <body-file>\n'); process.exit(1); }
|
||||
const raw = fs.readFileSync(bodyFile, 'utf-8');
|
||||
// The body file may be a full report. The caller passes the meta as
|
||||
// a JSON object on stdin if it wants structured frontmatter; otherwise
|
||||
// we write with minimal metadata.
|
||||
let meta = {};
|
||||
const metaArg = process.env.IMPECCABLE_CRITIQUE_META;
|
||||
if (metaArg) {
|
||||
try { meta = JSON.parse(metaArg); } catch { /* ignore */ }
|
||||
}
|
||||
const out = writeSnapshot({ slug, meta, body: raw });
|
||||
process.stdout.write(`${out}\n`);
|
||||
return;
|
||||
}
|
||||
case 'latest': {
|
||||
const latest = readLatestSnapshot(args[0]);
|
||||
if (!latest) { process.exit(2); }
|
||||
process.stdout.write(latest.body);
|
||||
return;
|
||||
}
|
||||
case 'trend': {
|
||||
const rows = readTrend(args[0], { limit: args[1] ? Number(args[1]) : 5 });
|
||||
process.stdout.write(JSON.stringify(rows, null, 2) + '\n');
|
||||
return;
|
||||
}
|
||||
default:
|
||||
process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend> [args]\n');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main(process.argv.slice(2));
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import path from 'node:path';
|
||||
|
||||
export const IMPECCABLE_DIR = '.impeccable';
|
||||
export const LIVE_DIR = 'live';
|
||||
export const CRITIQUE_DIR = 'critique';
|
||||
|
||||
export function getImpeccableDir(cwd = process.cwd()) {
|
||||
return path.join(cwd, IMPECCABLE_DIR);
|
||||
@@ -96,6 +97,10 @@ export function getLiveAnnotationsDir(cwd = process.cwd()) {
|
||||
return path.join(getLiveDir(cwd), 'annotations');
|
||||
}
|
||||
|
||||
export function getCritiqueDir(cwd = process.cwd()) {
|
||||
return path.join(getImpeccableDir(cwd), CRITIQUE_DIR);
|
||||
}
|
||||
|
||||
export function getLegacyLiveAnnotationsDir(cwd = process.cwd()) {
|
||||
return path.join(cwd, '.impeccable-live', 'annotations');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 3.0.7
|
||||
version: 3.1.0
|
||||
license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution.
|
||||
allowed-tools:
|
||||
- Bash(npx impeccable *)
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# Codex: Visual Direction & Asset Production
|
||||
|
||||
This file is loaded by `/impeccable craft` when the harness has native image generation (currently Codex via `image_gen`). Other harnesses skip it. It covers the two craft steps that depend on real image generation: landing the visual direction, and producing the raster assets the implementation will compose.
|
||||
|
||||
Read this *before* generating any images. The order matters, and the per-step user pauses are what keep generated imagery from drifting away from the brief.
|
||||
|
||||
## Step A: Explore Directions with the User
|
||||
|
||||
Before generating anything, run a brief direction conversation grounded in the shape brief.
|
||||
|
||||
Ask **2-3 targeted questions** about visual lane, color strategy, atmosphere, and named anchor references. Don't enumerate generic menus; tie each question to the shape brief's answers. Example shape-grounded questions:
|
||||
|
||||
- "Brief says 'editorial restraint, Klim-adjacent.' Are we closer to a quiet specimen page or a magazine-spread feel with hero imagery?"
|
||||
- "Palette strategy from shape was 'Committed.' Want it warm-grounded (deep oxblood + cream) or cool-grounded (slate + paper white)?"
|
||||
|
||||
Stop and wait for answers. These pin the palette before any pixel gets generated.
|
||||
|
||||
## Step B: Generate the Brand Palette First
|
||||
|
||||
Generate **one** palette artifact before any mocks. This is a small, focused image: typography pairing on the chosen background, primary + accent color swatches, one signature ornament or motif. Single image, single pass.
|
||||
|
||||
Why palette first: mocks generated against a vague color sense produce noise that drowns out the structural decisions. A confirmed palette is the first concrete contract for everything downstream.
|
||||
|
||||
Show the palette to the user. Ask one question: "This is the palette I'm locking in for the mocks. Confirm, or call out what to shift?"
|
||||
|
||||
Wait for confirmation. Do not generate mocks against an unconfirmed palette.
|
||||
|
||||
## Step C: Generate 1-3 Visual Mocks Against the Palette
|
||||
|
||||
Once the palette is confirmed, generate **1 to 3** high-fidelity north-star comps. Each mock must use the confirmed palette and typography. Mocks differ in *structural* direction (hierarchy, topology, density, composition), not in color or motif.
|
||||
|
||||
- Brand work: push visual identity, composition, mood, and signature motifs.
|
||||
- Product work: push hierarchy, topology, density, tone, grounded in realistic product structure.
|
||||
- Landing pages and long-form brand surfaces: show enough of the second fold to establish the system beyond the hero.
|
||||
|
||||
Use the `image_gen` tool directly (or via the imagegen skill when available). Don't ask the user to install anything.
|
||||
|
||||
## Step D: Approval Loop
|
||||
|
||||
Show the comps. Ask what carries forward. Iterate until **one direction is approved** or the user explicitly delegates.
|
||||
|
||||
If the user delegates, pick the strongest direction and explain it from the brief, not personal taste.
|
||||
|
||||
Before moving to assets, summarize what to carry into code and what *not* to literalize from the mock. This is the handoff between visual exploration and semantic implementation.
|
||||
|
||||
## Step E: Mock Fidelity Inventory
|
||||
|
||||
Inventory the approved mock's major visible ingredients. For each, decide implementation: semantic HTML/CSS/SVG, generated raster, sourced raster, icon library, canvas/WebGL, or accepted omission.
|
||||
|
||||
Common ingredients to inventory:
|
||||
|
||||
- Hero silhouette and dominant composition
|
||||
- Signature motifs (planets, devices, portraits, charts, route lines, insets, badges, etc.)
|
||||
- Nav and primary CTA treatment
|
||||
- Section sequence, especially the second fold
|
||||
- Image-native content the concept depends on
|
||||
- Typography, density, color/material treatment, motion cues
|
||||
|
||||
Treat the mock as a north star, not a screenshot to trace. Don't rasterize core UI text. But if the live result lacks the mock's major ingredients, the implementation is wrong.
|
||||
|
||||
If a photographic, architectural, product, or place-led mock becomes generic CSS scenery, decorative diagrams, bullets, or copy, stop and fix it. That's a broken implementation, not a harmless interpretation.
|
||||
|
||||
Don't substitute a different hero composition or visual driver post-approval without user sign-off.
|
||||
|
||||
## Step F: Asset Slicing via the Asset Producer
|
||||
|
||||
Raster ingredients identified in Step E need clean production assets. Use the bundled `impeccable_asset_producer` subagent rather than producing inline.
|
||||
|
||||
Spawn it as a scoped subagent. If you do not have explicit permission to use agents, stop and ask:
|
||||
|
||||
```text
|
||||
Asset production will work better as a scoped subagent job. Should I spawn the Impeccable asset producer subagent for this step?
|
||||
```
|
||||
|
||||
Pass to the agent:
|
||||
|
||||
- Approved mock path or screenshot reference
|
||||
- Crop paths or a contact sheet with crop ids
|
||||
- Output directory
|
||||
- Required dimensions, format, transparency needs
|
||||
- Avoid list
|
||||
- Notes on what should remain semantic HTML/CSS/SVG instead of raster
|
||||
|
||||
Attach image generation capability to the spawned agent when the harness supports it. Do **not** load image-generation reference material into the parent thread.
|
||||
|
||||
Inline asset production is allowed only if the user declines subagents, the harness cannot spawn the authorized agent, or the user explicitly asks for single-thread mode.
|
||||
|
||||
Prefer HTML/CSS/SVG/canvas when they can credibly reproduce an ingredient; reach for real, generated, or stock imagery when the mock or subject matter calls for actual visual content.
|
||||
|
||||
## After This File
|
||||
|
||||
Once Steps A through F are complete, return to `craft.md` Step 5 (Build to Production Quality). The implementation builds against the confirmed palette, approved mock, and the assets the producer wrote.
|
||||
@@ -51,64 +51,15 @@ Then add references based on the brief's needs:
|
||||
- Responsive requirements? Consult [responsive-design.md](responsive-design.md)
|
||||
- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.md)
|
||||
|
||||
## Step 3: Land the Visual Direction (Capability-Gated)
|
||||
## Step 3: Visual Direction & Assets (Harness-Gated)
|
||||
|
||||
Generate high-fidelity visual comps before implementation when all three are true:
|
||||
If the harness has **native image generation** (currently Codex via `image_gen`), this step is mandatory. **Stop and load [codex.md](codex.md)**. It covers palette generation, mock exploration, the approval loop, mock-fidelity inventory, and asset slicing via the `impeccable_asset_producer` subagent. Follow Steps A-F in that file, then return here for Step 4.
|
||||
|
||||
- The work is net-new or visually open-ended enough that composition exploration will improve the build.
|
||||
- The brief's scope is mid-fi, high-fi, or production-ready.
|
||||
- The harness gives you native image generation (Codex's `image_gen`, an equivalent MCP tool). Don't ask the user to set up external APIs.
|
||||
If the harness lacks native image generation, **state in one line that the visual-direction-by-generation step is being skipped because the harness lacks native image generation, then proceed**. The one-line announcement is required; it forces a conscious decision instead of letting the step quietly evaporate. The brief is your only visual reference. Implement directly from it, treating any named anchor references and the brief's "Design Direction" as the contract.
|
||||
|
||||
When met, this step is mandatory for both brand and product work. If image generation isn't available, skip silently. "The eventual UI is semantic/code-native/accessible" is not a reason to skip; those are implementation requirements, not exploration ones.
|
||||
Whether you generated mocks or not: don't replace required imagery with generic cards, bullets, emoji, fake metrics, decorative CSS panels, or filler copy. Image-led briefs (restaurants, hotels, magazines, photography, hobbyist communities, food, travel, fashion, product) need real or sourced imagery in the build, not CSS scenery.
|
||||
|
||||
### What to generate
|
||||
|
||||
Generate **1 to 3** high-fidelity north-star comps from the confirmed brief. If shape already produced direction probes, resolve the winning lane further, not an unrelated exploration. Comps must differ in primary visual direction, not just color.
|
||||
|
||||
- Brand work: push visual identity, composition, and mood aggressively.
|
||||
- Product work: push hierarchy, topology, density, tone, grounded in realistic product structure.
|
||||
- Landing pages and long-form brand surfaces: show enough of the second fold to establish the system beyond the hero.
|
||||
|
||||
### Approval loop
|
||||
|
||||
Show the comps and ask what carries forward. Iterate until one direction is approved or the user delegates. If the user delegates, pick the strongest direction and explain it from the brief, not personal taste.
|
||||
|
||||
Before Step 4, summarize what to carry into code and what **not** to literalize from the mock. This is the handoff between visual exploration and semantic implementation.
|
||||
|
||||
### Mock fidelity inventory
|
||||
|
||||
Inventory the approved mock's major visible ingredients:
|
||||
|
||||
- Hero silhouette and dominant composition
|
||||
- Signature motifs (planets, devices, portraits, charts, route lines, insets, badges, etc.)
|
||||
- Nav and primary CTA treatment
|
||||
- Section sequence, especially the second fold
|
||||
- Image-native content the concept depends on
|
||||
- Typography, density, color/material treatment, motion cues
|
||||
|
||||
For each, decide implementation: semantic HTML/CSS/SVG, generated asset, sourced asset, icon library, canvas/WebGL, or accepted omission. Don't substitute a different hero composition or visual driver post-approval without user sign-off.
|
||||
|
||||
If a photographic, architectural, product, or place-led mock becomes generic CSS scenery / decorative diagrams / bullets / copy, stop and fix it. That's a broken implementation, not a harmless interpretation.
|
||||
|
||||
Treat the mock as a north star, not a screenshot to trace. Don't rasterize core UI text. But if the live result lacks the mock's major ingredients, the implementation is wrong.
|
||||
|
||||
## Step 4: Asset Extraction (Need-Gated)
|
||||
|
||||
If the approved direction needs raster assets, create them before building. Do not replace required imagery with generic cards, bullets, emoji, fake metrics, decorative CSS panels, or filler copy.
|
||||
|
||||
Use the native asset producer (`impeccable_asset_producer` in Codex, `impeccable-asset-producer` in Claude Code) to create clean assets from the hi-fi mock and crops. If you do not have explicit permission to use agents, stop and ask:
|
||||
|
||||
```text
|
||||
Asset production will work better as a scoped subagent job. Should I spawn the Impeccable asset producer subagent for this step?
|
||||
```
|
||||
|
||||
Do not skip asset production or silently do it inline. Inline asset production is allowed only if the user declines subagents, the harness cannot spawn the authorized agent, or the user explicitly asks for single-thread mode.
|
||||
|
||||
Pass the approved mock, crop/contact-sheet paths, output directory, dimensions/formats, transparency needs, constraints, and avoid list to the asset producer. Attach image generation capability to the spawned agent when the harness supports it; do not load image-generation reference material into the parent thread first.
|
||||
|
||||
Prefer HTML/CSS/SVG/canvas when they can credibly reproduce an ingredient; use real/generated/stock imagery when the mock or subject matter calls for actual visual content.
|
||||
|
||||
## Step 5: Build to Production Quality
|
||||
## Step 4: Build to Production Quality
|
||||
|
||||
Implement the feature following the design brief. Build in passes so structure, visual system, states, motion/media, and responsive behavior each get deliberate attention. The list below is the definition of done, not inspiration.
|
||||
|
||||
@@ -130,7 +81,7 @@ Implement the feature following the design brief. Build in passes so structure,
|
||||
- **Technically clean.** Production build passes, no console errors, no avoidable layout shift, no needless dependencies, no broken asset paths.
|
||||
- **Ask when uncertain.** If a discovery materially changes the brief or approved direction, stop and ask. Don't guess.
|
||||
|
||||
## Step 6: Iterate Visually
|
||||
## Step 5: Iterate Visually
|
||||
|
||||
Look at what you built like a designer would. Your eyes are whatever the harness gives you: a connected browser, a screenshotting tool, Playwright, or asking the user. Use them for responsive testing (mobile, tablet, desktop minimum) and general visual validation.
|
||||
|
||||
@@ -144,7 +95,7 @@ Actively check: responsive behavior (composes, not shrinks), every state (empty
|
||||
|
||||
Detector or QA output is defect evidence only; never proof the work is finished.
|
||||
|
||||
## Step 7: Present
|
||||
## Step 6: Present
|
||||
|
||||
Present the result to the user:
|
||||
- Show the feature in its primary state
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
> **Additional context needed**: what the interface is trying to accomplish.
|
||||
|
||||
### Setup: Resolve Target and Load Ignore List
|
||||
|
||||
Before gathering assessments, do two small bookkeeping steps. They cost almost nothing and they're what makes critique iterative across runs.
|
||||
|
||||
1. **Resolve the primary artifact.** The user's phrasing ("the homepage", "the pricing flow") is not stable enough to track across runs. Resolve it to a concrete file path or URL: the same one you'd already need to scan code or open in a browser. Examples:
|
||||
- "the homepage" → `site/pages/index.astro` (or `http://localhost:3000/` if you're inspecting live)
|
||||
- "the settings modal" → the primary component file (e.g., `src/components/Settings.tsx`)
|
||||
- "this page" → the URL or the page's source file
|
||||
Prefer the source file path over the dev-server URL when both exist; ports drift between runs (`bun dev` vs `bun preview`), file paths don't.
|
||||
|
||||
2. **Compute the slug.** Run:
|
||||
```bash
|
||||
node .pi/skills/impeccable/scripts/critique-storage.mjs slug "<resolved-path-or-url>"
|
||||
```
|
||||
Keep the printed slug. It identifies this target's stream across runs. If the command exits non-zero ("no stable slug for input"), skip persistence for this run and tell the user; the trend won't update but the critique still goes ahead.
|
||||
|
||||
3. **Read the ignore list** at `.impeccable/critique/ignore.md` if it exists. Plain markdown; each non-empty, non-comment line is something the user has marked as "do not re-raise" (deferred tradeoffs, designer-intended deviations, detector false-positives the user accepts). When a finding's text matches a line here (case-insensitive substring against rule name or snippet), **drop it silently**. Do not mention it in the report. This is the ONLY input critique consumes from prior runs; anchoring on prior findings would defeat the point of independent assessment.
|
||||
|
||||
### Gather Assessments
|
||||
|
||||
Launch two independent assessments. **Neither may see the other's output.** This isolation is what makes the combined score honest. Running both in one head silently anchors them to each other; do not shortcut it for cost, speed, or context-size reasons.
|
||||
@@ -164,6 +182,36 @@ Provocative questions that might unlock better solutions:
|
||||
- Prioritize ruthlessly. If everything is important, nothing is.
|
||||
- Don't soften criticism. Developers need honest feedback to ship great design.
|
||||
|
||||
### Persist the Snapshot
|
||||
|
||||
Once the report above is finalized, write it to `.impeccable/critique/` so the user can refer back, and so `/impeccable polish` can pick up the priority issues without a copy-paste.
|
||||
|
||||
Skip this step if the Setup slug was null (vague or root-level target).
|
||||
|
||||
1. **Write the body to a temp file** so you can pipe it to the helper. Use the full report (heuristic table, anti-patterns verdict, priority issues, persona red flags) but stop before the "Ask the User" / "Recommended Actions" sections that come later.
|
||||
|
||||
2. **Pass the structured metadata** through `IMPECCABLE_CRITIQUE_META` (JSON), then run the write command:
|
||||
```bash
|
||||
IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"p0_count":<n>,"p1_count":<n>}' \
|
||||
node .pi/skills/impeccable/scripts/critique-storage.mjs write <slug> <body-file>
|
||||
```
|
||||
The helper prints the absolute path it wrote.
|
||||
|
||||
3. **Read the trend** for context:
|
||||
```bash
|
||||
node .pi/skills/impeccable/scripts/critique-storage.mjs trend <slug> 5
|
||||
```
|
||||
This returns a JSON array of the last 5 frontmatter entries (including the one you just wrote).
|
||||
|
||||
4. **Append a single line to the user-visible output**, after the report and before the questions:
|
||||
|
||||
> **Trend for `<slug>` (last 5 runs): 24 → 28 → 32 → 29 → 32**
|
||||
> Wrote `.impeccable/critique/<filename>`.
|
||||
|
||||
If this is the first run for the slug, the trend is just one score; say so: "First run for this target, no trend yet."
|
||||
|
||||
This is fire-and-forget. Do not show the user the helper's JSON output; only the human-readable trend line and the written path. Failures here should not block the rest of the flow; print the error and move on.
|
||||
|
||||
### Ask the User
|
||||
|
||||
**After presenting findings**, use targeted questions based on what was actually found. ask the user directly to clarify what you cannot infer. These answers will shape the action plan.
|
||||
|
||||
@@ -35,7 +35,14 @@ Understand the current state and goals before touching anything:
|
||||
- Loading and transition smoothness
|
||||
- Information architecture and flow drift (does this feature reveal complexity the way neighboring features do?)
|
||||
|
||||
4. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent; never perfect one corner while leaving another rough.
|
||||
4. **Pull in any prior critique** (optional signal): If `/impeccable critique` has been run on the same target, its priority issues are a useful prior for what to address first. Resolve the target to a file path or URL, then:
|
||||
```bash
|
||||
slug=$(node .pi/skills/impeccable/scripts/critique-storage.mjs slug "<resolved>")
|
||||
node .pi/skills/impeccable/scripts/critique-storage.mjs latest "$slug"
|
||||
```
|
||||
Exit 0 with body = found; fold the P0/P1 items into your polish list and mention the snapshot path so the user sees what you read. Exit 2 = no snapshot, continue without it. The critique is one input among many. Do your own pass either way.
|
||||
|
||||
5. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent; never perfect one corner while leaving another rough.
|
||||
|
||||
**CRITICAL**: Polish is the last step, not the first. Don't polish work that's not functionally complete.
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ After the discovery interview, generate a small set of visual direction probes *
|
||||
- The requested fidelity is **mid-fi, high-fi, or production-ready**. Skip for sketch-only planning.
|
||||
- The current harness gives you native image generation (Codex's `image_gen`, an equivalent MCP tool, or similar). Don't ask the user to install APIs or tooling.
|
||||
|
||||
When those conditions are met, this step is mandatory. If image generation isn't natively available, skip silently and proceed; don't announce the skip.
|
||||
When those conditions are met, this step is mandatory. If image generation isn't natively available, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed. The one-line announcement is required, not optional; it forces a conscious decision instead of letting the step quietly evaporate.
|
||||
|
||||
Use probes to explore visual lanes, not to replace the brief.
|
||||
|
||||
@@ -107,7 +107,7 @@ The probes should differ in primary visual direction (hierarchy, topology, densi
|
||||
- Do **not** treat generated imagery as final UX specification, final copy, or final accessibility behavior.
|
||||
- Do **not** use this step for minor refinements of existing work. It's for shaping a new surface or clarifying a big directional choice.
|
||||
|
||||
If image generation isn't natively available, skip this phase silently and proceed.
|
||||
If image generation isn't natively available, announce the skip in one line and proceed to the design brief.
|
||||
|
||||
## Phase 2: Design Brief
|
||||
|
||||
@@ -120,7 +120,7 @@ After the interview and any required probes, present a brief and **end your resp
|
||||
|
||||
Don't pad a clear brief into a long one to look thorough. A 70-line brief restating answers the user just gave is noise, not rigor. Equally, don't skip the confirmation pause to look efficient: the pause is the point.
|
||||
|
||||
If the user already said "approved" or "go" during discovery for the *exact direction you'd present*, that counts as confirmation; you may proceed without asking again. But if your brief adds anything the user hasn't seen and approved, you must stop and confirm.
|
||||
Present the brief, then **stop and wait for explicit confirmation**. You are not the judge of whether the user already approved. Even when the brief feels obviously right, ask once and wait. The pause is what separates shape from premature implementation.
|
||||
|
||||
### Brief Structure
|
||||
|
||||
@@ -158,8 +158,8 @@ Anything genuinely unresolved. Don't list "open questions" you've already recomm
|
||||
|
||||
---
|
||||
|
||||
ask the user directly to clarify what you cannot infer.
|
||||
ask the user directly to clarify what you cannot infer. Ask for explicit confirmation of the brief before finishing.
|
||||
|
||||
If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the user confirms direction (or the user already gave a clear go during discovery, which counts).
|
||||
If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the user confirms direction.
|
||||
|
||||
Once confirmed, the brief is complete. The user can now hand it to /impeccable, or use it to guide any other implementation approach. (If the user wants the full discovery-then-build flow in one step, they should use /impeccable craft instead, which runs this command internally.)
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Critique persistence helper.
|
||||
*
|
||||
* Each run of /impeccable critique writes a per-target snapshot to
|
||||
* .impeccable/critique/<timestamp>__<slug>.md
|
||||
* with a small YAML frontmatter carrying the score + P0/P1 counts.
|
||||
*
|
||||
* /impeccable polish reads the latest matching snapshot at start as its
|
||||
* fix backlog. No other skill auto-reads critique output.
|
||||
*
|
||||
* The slug is derived mechanically from the *resolved* primary artifact
|
||||
* (file path or URL), never from the user's natural-language phrasing.
|
||||
* Slug stability across runs is what lets the trend display work.
|
||||
*
|
||||
* CLI entry points (called from skill instructions):
|
||||
* node critique-storage.mjs slug <resolved-target>
|
||||
* node critique-storage.mjs write <slug> <snapshot-body-file>
|
||||
* node critique-storage.mjs latest <slug>
|
||||
* node critique-storage.mjs trend <slug> [limit]
|
||||
*
|
||||
* Note: there is intentionally no `ignore` subcommand. ignore.md is a plain
|
||||
* markdown file; the model reads it directly with its file-read tool. This
|
||||
* helper only exists for operations the model can't trivially do inline
|
||||
* (normalizing paths, generating filenames, globbing + parsing frontmatter).
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { getCritiqueDir } from './impeccable-paths.mjs';
|
||||
|
||||
const SLUG_MAX = 50;
|
||||
|
||||
/**
|
||||
* Mechanically derive a slug from a resolved target. Returns null if the
|
||||
* input doesn't look like a stable identifier (empty, project root, etc).
|
||||
*
|
||||
* Accepts file paths and URLs. The model resolves "the homepage" to a
|
||||
* concrete artifact before calling this — we never slug a natural-language
|
||||
* phrase.
|
||||
*/
|
||||
export function slugFromTarget(resolved, { cwd = process.cwd() } = {}) {
|
||||
if (!resolved || typeof resolved !== 'string') return null;
|
||||
const trimmed = resolved.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
// URL
|
||||
if (/^https?:\/\//i.test(trimmed)) {
|
||||
let url;
|
||||
try { url = new URL(trimmed); } catch { return null; }
|
||||
const hostPath = `${url.hostname}${url.pathname}`;
|
||||
return kebab(hostPath);
|
||||
}
|
||||
|
||||
// File path. Make it project-relative so two devs critiquing the same
|
||||
// checkout get the same slug regardless of where their repo is cloned.
|
||||
const abs = path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
|
||||
let rel = path.relative(cwd, abs);
|
||||
// If the target is outside cwd, fall back to the basename so we still
|
||||
// produce a stable slug (vs the absolute path, which would include
|
||||
// home dirs / usernames).
|
||||
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
||||
rel = path.basename(abs);
|
||||
}
|
||||
if (!rel || rel === '.' || rel === '') return null;
|
||||
return kebab(rel);
|
||||
}
|
||||
|
||||
function kebab(s) {
|
||||
const slug = s
|
||||
.toLowerCase()
|
||||
.replace(/[/\\.]+/g, '-')
|
||||
.replace(/[^a-z0-9-]+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
if (!slug) return null;
|
||||
// Cap from the tail — the tail (filename) is more identifying than the
|
||||
// top-level directory.
|
||||
return slug.length <= SLUG_MAX ? slug : slug.slice(slug.length - SLUG_MAX).replace(/^-/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filename-safe UTC ISO timestamp: hyphens for separators, trailing Z.
|
||||
* Plain colons aren't allowed on Windows filesystems.
|
||||
*/
|
||||
export function nowFilenameStamp(date = new Date()) {
|
||||
const iso = date.toISOString(); // 2026-05-12T18:30:00.123Z
|
||||
return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z');
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a snapshot for `slug`. `meta` carries the small structured frontmatter
|
||||
* keys read back by readTrend(). `body` is the human-readable critique
|
||||
* report (everything below the frontmatter).
|
||||
*
|
||||
* Returns the absolute path written.
|
||||
*/
|
||||
export function writeSnapshot({ slug, meta, body, cwd = process.cwd(), now = new Date() }) {
|
||||
if (!slug) throw new Error('writeSnapshot requires a slug');
|
||||
const dir = getCritiqueDir(cwd);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const timestamp = nowFilenameStamp(now);
|
||||
const filePath = path.join(dir, `${timestamp}__${slug}.md`);
|
||||
// Spread `meta` first so internally computed `timestamp` and `slug`
|
||||
// always win. Otherwise a caller-supplied meta blob (parsed from the
|
||||
// IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the
|
||||
// filename in disagreement with its frontmatter and corrupting trends.
|
||||
const front = serializeFrontmatter({ ...meta, timestamp, slug });
|
||||
fs.writeFileSync(filePath, `${front}\n${body.trim()}\n`, 'utf-8');
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function serializeFrontmatter(obj) {
|
||||
const lines = ['---'];
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
const str = typeof value === 'string' ? value : String(value);
|
||||
// Quote strings that contain : or # to keep parsing simple.
|
||||
const needsQuotes = typeof value === 'string' && /[:#]/.test(str);
|
||||
lines.push(`${key}: ${needsQuotes ? JSON.stringify(str) : str}`);
|
||||
}
|
||||
lines.push('---');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function parseFrontmatter(text) {
|
||||
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
if (!match) return {};
|
||||
const out = {};
|
||||
for (const line of match[1].split(/\r?\n/)) {
|
||||
const colon = line.indexOf(':');
|
||||
if (colon < 0) continue;
|
||||
const key = line.slice(0, colon).trim();
|
||||
let value = line.slice(colon + 1).trim();
|
||||
if (/^".*"$/.test(value)) {
|
||||
try { value = JSON.parse(value); } catch { /* leave as-is */ }
|
||||
} else if (/^-?\d+$/.test(value)) {
|
||||
value = Number(value);
|
||||
}
|
||||
out[key] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all snapshot files for `slug`, sorted oldest → newest.
|
||||
*/
|
||||
function listSnapshotsForSlug(slug, cwd) {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const suffix = `__${slug}.md`;
|
||||
return fs.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(suffix))
|
||||
.sort()
|
||||
.map((f) => path.join(dir, f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent snapshot for `slug`, or null. Polish reads this
|
||||
* to find its fix backlog when the slug matches.
|
||||
*/
|
||||
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
if (!all.length) return null;
|
||||
const latest = all[all.length - 1];
|
||||
const body = fs.readFileSync(latest, 'utf-8');
|
||||
return { path: latest, body, meta: parseFrontmatter(body) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last `limit` snapshots' frontmatter, oldest → newest.
|
||||
* Critique appends a one-line trend to its output using this.
|
||||
*/
|
||||
export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
const slice = all.slice(-limit);
|
||||
return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8')));
|
||||
}
|
||||
|
||||
// ---- CLI ---------------------------------------------------------------
|
||||
|
||||
function main(argv) {
|
||||
const [cmd, ...args] = argv;
|
||||
switch (cmd) {
|
||||
case 'slug': {
|
||||
const slug = slugFromTarget(args[0]);
|
||||
if (!slug) { process.stderr.write('no stable slug for input\n'); process.exit(1); }
|
||||
process.stdout.write(`${slug}\n`);
|
||||
return;
|
||||
}
|
||||
case 'write': {
|
||||
const [slug, bodyFile] = args;
|
||||
if (!slug || !bodyFile) { process.stderr.write('usage: write <slug> <body-file>\n'); process.exit(1); }
|
||||
const raw = fs.readFileSync(bodyFile, 'utf-8');
|
||||
// The body file may be a full report. The caller passes the meta as
|
||||
// a JSON object on stdin if it wants structured frontmatter; otherwise
|
||||
// we write with minimal metadata.
|
||||
let meta = {};
|
||||
const metaArg = process.env.IMPECCABLE_CRITIQUE_META;
|
||||
if (metaArg) {
|
||||
try { meta = JSON.parse(metaArg); } catch { /* ignore */ }
|
||||
}
|
||||
const out = writeSnapshot({ slug, meta, body: raw });
|
||||
process.stdout.write(`${out}\n`);
|
||||
return;
|
||||
}
|
||||
case 'latest': {
|
||||
const latest = readLatestSnapshot(args[0]);
|
||||
if (!latest) { process.exit(2); }
|
||||
process.stdout.write(latest.body);
|
||||
return;
|
||||
}
|
||||
case 'trend': {
|
||||
const rows = readTrend(args[0], { limit: args[1] ? Number(args[1]) : 5 });
|
||||
process.stdout.write(JSON.stringify(rows, null, 2) + '\n');
|
||||
return;
|
||||
}
|
||||
default:
|
||||
process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend> [args]\n');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main(process.argv.slice(2));
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import path from 'node:path';
|
||||
|
||||
export const IMPECCABLE_DIR = '.impeccable';
|
||||
export const LIVE_DIR = 'live';
|
||||
export const CRITIQUE_DIR = 'critique';
|
||||
|
||||
export function getImpeccableDir(cwd = process.cwd()) {
|
||||
return path.join(cwd, IMPECCABLE_DIR);
|
||||
@@ -96,6 +97,10 @@ export function getLiveAnnotationsDir(cwd = process.cwd()) {
|
||||
return path.join(getLiveDir(cwd), 'annotations');
|
||||
}
|
||||
|
||||
export function getCritiqueDir(cwd = process.cwd()) {
|
||||
return path.join(getImpeccableDir(cwd), CRITIQUE_DIR);
|
||||
}
|
||||
|
||||
export function getLegacyLiveAnnotationsDir(cwd = process.cwd()) {
|
||||
return path.join(cwd, '.impeccable-live', 'annotations');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 3.0.7
|
||||
version: 3.1.0
|
||||
user-invocable: true
|
||||
argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · teach|document|extract|live] [target]"
|
||||
license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution.
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# Codex: Visual Direction & Asset Production
|
||||
|
||||
This file is loaded by `/impeccable craft` when the harness has native image generation (currently Codex via `image_gen`). Other harnesses skip it. It covers the two craft steps that depend on real image generation: landing the visual direction, and producing the raster assets the implementation will compose.
|
||||
|
||||
Read this *before* generating any images. The order matters, and the per-step user pauses are what keep generated imagery from drifting away from the brief.
|
||||
|
||||
## Step A: Explore Directions with the User
|
||||
|
||||
Before generating anything, run a brief direction conversation grounded in the shape brief.
|
||||
|
||||
Ask **2-3 targeted questions** about visual lane, color strategy, atmosphere, and named anchor references. Don't enumerate generic menus; tie each question to the shape brief's answers. Example shape-grounded questions:
|
||||
|
||||
- "Brief says 'editorial restraint, Klim-adjacent.' Are we closer to a quiet specimen page or a magazine-spread feel with hero imagery?"
|
||||
- "Palette strategy from shape was 'Committed.' Want it warm-grounded (deep oxblood + cream) or cool-grounded (slate + paper white)?"
|
||||
|
||||
Stop and wait for answers. These pin the palette before any pixel gets generated.
|
||||
|
||||
## Step B: Generate the Brand Palette First
|
||||
|
||||
Generate **one** palette artifact before any mocks. This is a small, focused image: typography pairing on the chosen background, primary + accent color swatches, one signature ornament or motif. Single image, single pass.
|
||||
|
||||
Why palette first: mocks generated against a vague color sense produce noise that drowns out the structural decisions. A confirmed palette is the first concrete contract for everything downstream.
|
||||
|
||||
Show the palette to the user. Ask one question: "This is the palette I'm locking in for the mocks. Confirm, or call out what to shift?"
|
||||
|
||||
Wait for confirmation. Do not generate mocks against an unconfirmed palette.
|
||||
|
||||
## Step C: Generate 1-3 Visual Mocks Against the Palette
|
||||
|
||||
Once the palette is confirmed, generate **1 to 3** high-fidelity north-star comps. Each mock must use the confirmed palette and typography. Mocks differ in *structural* direction (hierarchy, topology, density, composition), not in color or motif.
|
||||
|
||||
- Brand work: push visual identity, composition, mood, and signature motifs.
|
||||
- Product work: push hierarchy, topology, density, tone, grounded in realistic product structure.
|
||||
- Landing pages and long-form brand surfaces: show enough of the second fold to establish the system beyond the hero.
|
||||
|
||||
Use the `image_gen` tool directly (or via the imagegen skill when available). Don't ask the user to install anything.
|
||||
|
||||
## Step D: Approval Loop
|
||||
|
||||
Show the comps. Ask what carries forward. Iterate until **one direction is approved** or the user explicitly delegates.
|
||||
|
||||
If the user delegates, pick the strongest direction and explain it from the brief, not personal taste.
|
||||
|
||||
Before moving to assets, summarize what to carry into code and what *not* to literalize from the mock. This is the handoff between visual exploration and semantic implementation.
|
||||
|
||||
## Step E: Mock Fidelity Inventory
|
||||
|
||||
Inventory the approved mock's major visible ingredients. For each, decide implementation: semantic HTML/CSS/SVG, generated raster, sourced raster, icon library, canvas/WebGL, or accepted omission.
|
||||
|
||||
Common ingredients to inventory:
|
||||
|
||||
- Hero silhouette and dominant composition
|
||||
- Signature motifs (planets, devices, portraits, charts, route lines, insets, badges, etc.)
|
||||
- Nav and primary CTA treatment
|
||||
- Section sequence, especially the second fold
|
||||
- Image-native content the concept depends on
|
||||
- Typography, density, color/material treatment, motion cues
|
||||
|
||||
Treat the mock as a north star, not a screenshot to trace. Don't rasterize core UI text. But if the live result lacks the mock's major ingredients, the implementation is wrong.
|
||||
|
||||
If a photographic, architectural, product, or place-led mock becomes generic CSS scenery, decorative diagrams, bullets, or copy, stop and fix it. That's a broken implementation, not a harmless interpretation.
|
||||
|
||||
Don't substitute a different hero composition or visual driver post-approval without user sign-off.
|
||||
|
||||
## Step F: Asset Slicing via the Asset Producer
|
||||
|
||||
Raster ingredients identified in Step E need clean production assets. Use the bundled `impeccable_asset_producer` subagent rather than producing inline.
|
||||
|
||||
Spawn it as a scoped subagent. If you do not have explicit permission to use agents, stop and ask:
|
||||
|
||||
```text
|
||||
Asset production will work better as a scoped subagent job. Should I spawn the Impeccable asset producer subagent for this step?
|
||||
```
|
||||
|
||||
Pass to the agent:
|
||||
|
||||
- Approved mock path or screenshot reference
|
||||
- Crop paths or a contact sheet with crop ids
|
||||
- Output directory
|
||||
- Required dimensions, format, transparency needs
|
||||
- Avoid list
|
||||
- Notes on what should remain semantic HTML/CSS/SVG instead of raster
|
||||
|
||||
Attach image generation capability to the spawned agent when the harness supports it. Do **not** load image-generation reference material into the parent thread.
|
||||
|
||||
Inline asset production is allowed only if the user declines subagents, the harness cannot spawn the authorized agent, or the user explicitly asks for single-thread mode.
|
||||
|
||||
Prefer HTML/CSS/SVG/canvas when they can credibly reproduce an ingredient; reach for real, generated, or stock imagery when the mock or subject matter calls for actual visual content.
|
||||
|
||||
## After This File
|
||||
|
||||
Once Steps A through F are complete, return to `craft.md` Step 5 (Build to Production Quality). The implementation builds against the confirmed palette, approved mock, and the assets the producer wrote.
|
||||
@@ -51,64 +51,15 @@ Then add references based on the brief's needs:
|
||||
- Responsive requirements? Consult [responsive-design.md](responsive-design.md)
|
||||
- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.md)
|
||||
|
||||
## Step 3: Land the Visual Direction (Capability-Gated)
|
||||
## Step 3: Visual Direction & Assets (Harness-Gated)
|
||||
|
||||
Generate high-fidelity visual comps before implementation when all three are true:
|
||||
If the harness has **native image generation** (currently Codex via `image_gen`), this step is mandatory. **Stop and load [codex.md](codex.md)**. It covers palette generation, mock exploration, the approval loop, mock-fidelity inventory, and asset slicing via the `impeccable_asset_producer` subagent. Follow Steps A-F in that file, then return here for Step 4.
|
||||
|
||||
- The work is net-new or visually open-ended enough that composition exploration will improve the build.
|
||||
- The brief's scope is mid-fi, high-fi, or production-ready.
|
||||
- The harness gives you native image generation (Codex's `image_gen`, an equivalent MCP tool). Don't ask the user to set up external APIs.
|
||||
If the harness lacks native image generation, **state in one line that the visual-direction-by-generation step is being skipped because the harness lacks native image generation, then proceed**. The one-line announcement is required; it forces a conscious decision instead of letting the step quietly evaporate. The brief is your only visual reference. Implement directly from it, treating any named anchor references and the brief's "Design Direction" as the contract.
|
||||
|
||||
When met, this step is mandatory for both brand and product work. If image generation isn't available, skip silently. "The eventual UI is semantic/code-native/accessible" is not a reason to skip; those are implementation requirements, not exploration ones.
|
||||
Whether you generated mocks or not: don't replace required imagery with generic cards, bullets, emoji, fake metrics, decorative CSS panels, or filler copy. Image-led briefs (restaurants, hotels, magazines, photography, hobbyist communities, food, travel, fashion, product) need real or sourced imagery in the build, not CSS scenery.
|
||||
|
||||
### What to generate
|
||||
|
||||
Generate **1 to 3** high-fidelity north-star comps from the confirmed brief. If shape already produced direction probes, resolve the winning lane further, not an unrelated exploration. Comps must differ in primary visual direction, not just color.
|
||||
|
||||
- Brand work: push visual identity, composition, and mood aggressively.
|
||||
- Product work: push hierarchy, topology, density, tone, grounded in realistic product structure.
|
||||
- Landing pages and long-form brand surfaces: show enough of the second fold to establish the system beyond the hero.
|
||||
|
||||
### Approval loop
|
||||
|
||||
Show the comps and ask what carries forward. Iterate until one direction is approved or the user delegates. If the user delegates, pick the strongest direction and explain it from the brief, not personal taste.
|
||||
|
||||
Before Step 4, summarize what to carry into code and what **not** to literalize from the mock. This is the handoff between visual exploration and semantic implementation.
|
||||
|
||||
### Mock fidelity inventory
|
||||
|
||||
Inventory the approved mock's major visible ingredients:
|
||||
|
||||
- Hero silhouette and dominant composition
|
||||
- Signature motifs (planets, devices, portraits, charts, route lines, insets, badges, etc.)
|
||||
- Nav and primary CTA treatment
|
||||
- Section sequence, especially the second fold
|
||||
- Image-native content the concept depends on
|
||||
- Typography, density, color/material treatment, motion cues
|
||||
|
||||
For each, decide implementation: semantic HTML/CSS/SVG, generated asset, sourced asset, icon library, canvas/WebGL, or accepted omission. Don't substitute a different hero composition or visual driver post-approval without user sign-off.
|
||||
|
||||
If a photographic, architectural, product, or place-led mock becomes generic CSS scenery / decorative diagrams / bullets / copy, stop and fix it. That's a broken implementation, not a harmless interpretation.
|
||||
|
||||
Treat the mock as a north star, not a screenshot to trace. Don't rasterize core UI text. But if the live result lacks the mock's major ingredients, the implementation is wrong.
|
||||
|
||||
## Step 4: Asset Extraction (Need-Gated)
|
||||
|
||||
If the approved direction needs raster assets, create them before building. Do not replace required imagery with generic cards, bullets, emoji, fake metrics, decorative CSS panels, or filler copy.
|
||||
|
||||
Use the native asset producer (`impeccable_asset_producer` in Codex, `impeccable-asset-producer` in Claude Code) to create clean assets from the hi-fi mock and crops. If you do not have explicit permission to use agents, stop and ask:
|
||||
|
||||
```text
|
||||
Asset production will work better as a scoped subagent job. Should I spawn the Impeccable asset producer subagent for this step?
|
||||
```
|
||||
|
||||
Do not skip asset production or silently do it inline. Inline asset production is allowed only if the user declines subagents, the harness cannot spawn the authorized agent, or the user explicitly asks for single-thread mode.
|
||||
|
||||
Pass the approved mock, crop/contact-sheet paths, output directory, dimensions/formats, transparency needs, constraints, and avoid list to the asset producer. Attach image generation capability to the spawned agent when the harness supports it; do not load image-generation reference material into the parent thread first.
|
||||
|
||||
Prefer HTML/CSS/SVG/canvas when they can credibly reproduce an ingredient; use real/generated/stock imagery when the mock or subject matter calls for actual visual content.
|
||||
|
||||
## Step 5: Build to Production Quality
|
||||
## Step 4: Build to Production Quality
|
||||
|
||||
Implement the feature following the design brief. Build in passes so structure, visual system, states, motion/media, and responsive behavior each get deliberate attention. The list below is the definition of done, not inspiration.
|
||||
|
||||
@@ -130,7 +81,7 @@ Implement the feature following the design brief. Build in passes so structure,
|
||||
- **Technically clean.** Production build passes, no console errors, no avoidable layout shift, no needless dependencies, no broken asset paths.
|
||||
- **Ask when uncertain.** If a discovery materially changes the brief or approved direction, stop and ask. Don't guess.
|
||||
|
||||
## Step 6: Iterate Visually
|
||||
## Step 5: Iterate Visually
|
||||
|
||||
Look at what you built like a designer would. Your eyes are whatever the harness gives you: a connected browser, a screenshotting tool, Playwright, or asking the user. Use them for responsive testing (mobile, tablet, desktop minimum) and general visual validation.
|
||||
|
||||
@@ -144,7 +95,7 @@ Actively check: responsive behavior (composes, not shrinks), every state (empty
|
||||
|
||||
Detector or QA output is defect evidence only; never proof the work is finished.
|
||||
|
||||
## Step 7: Present
|
||||
## Step 6: Present
|
||||
|
||||
Present the result to the user:
|
||||
- Show the feature in its primary state
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
> **Additional context needed**: what the interface is trying to accomplish.
|
||||
|
||||
### Setup: Resolve Target and Load Ignore List
|
||||
|
||||
Before gathering assessments, do two small bookkeeping steps. They cost almost nothing and they're what makes critique iterative across runs.
|
||||
|
||||
1. **Resolve the primary artifact.** The user's phrasing ("the homepage", "the pricing flow") is not stable enough to track across runs. Resolve it to a concrete file path or URL: the same one you'd already need to scan code or open in a browser. Examples:
|
||||
- "the homepage" → `site/pages/index.astro` (or `http://localhost:3000/` if you're inspecting live)
|
||||
- "the settings modal" → the primary component file (e.g., `src/components/Settings.tsx`)
|
||||
- "this page" → the URL or the page's source file
|
||||
Prefer the source file path over the dev-server URL when both exist; ports drift between runs (`bun dev` vs `bun preview`), file paths don't.
|
||||
|
||||
2. **Compute the slug.** Run:
|
||||
```bash
|
||||
node .qoder/skills/impeccable/scripts/critique-storage.mjs slug "<resolved-path-or-url>"
|
||||
```
|
||||
Keep the printed slug. It identifies this target's stream across runs. If the command exits non-zero ("no stable slug for input"), skip persistence for this run and tell the user; the trend won't update but the critique still goes ahead.
|
||||
|
||||
3. **Read the ignore list** at `.impeccable/critique/ignore.md` if it exists. Plain markdown; each non-empty, non-comment line is something the user has marked as "do not re-raise" (deferred tradeoffs, designer-intended deviations, detector false-positives the user accepts). When a finding's text matches a line here (case-insensitive substring against rule name or snippet), **drop it silently**. Do not mention it in the report. This is the ONLY input critique consumes from prior runs; anchoring on prior findings would defeat the point of independent assessment.
|
||||
|
||||
### Gather Assessments
|
||||
|
||||
Launch two independent assessments. **Neither may see the other's output.** This isolation is what makes the combined score honest. Running both in one head silently anchors them to each other; do not shortcut it for cost, speed, or context-size reasons.
|
||||
@@ -164,6 +182,36 @@ Provocative questions that might unlock better solutions:
|
||||
- Prioritize ruthlessly. If everything is important, nothing is.
|
||||
- Don't soften criticism. Developers need honest feedback to ship great design.
|
||||
|
||||
### Persist the Snapshot
|
||||
|
||||
Once the report above is finalized, write it to `.impeccable/critique/` so the user can refer back, and so `/impeccable polish` can pick up the priority issues without a copy-paste.
|
||||
|
||||
Skip this step if the Setup slug was null (vague or root-level target).
|
||||
|
||||
1. **Write the body to a temp file** so you can pipe it to the helper. Use the full report (heuristic table, anti-patterns verdict, priority issues, persona red flags) but stop before the "Ask the User" / "Recommended Actions" sections that come later.
|
||||
|
||||
2. **Pass the structured metadata** through `IMPECCABLE_CRITIQUE_META` (JSON), then run the write command:
|
||||
```bash
|
||||
IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"p0_count":<n>,"p1_count":<n>}' \
|
||||
node .qoder/skills/impeccable/scripts/critique-storage.mjs write <slug> <body-file>
|
||||
```
|
||||
The helper prints the absolute path it wrote.
|
||||
|
||||
3. **Read the trend** for context:
|
||||
```bash
|
||||
node .qoder/skills/impeccable/scripts/critique-storage.mjs trend <slug> 5
|
||||
```
|
||||
This returns a JSON array of the last 5 frontmatter entries (including the one you just wrote).
|
||||
|
||||
4. **Append a single line to the user-visible output**, after the report and before the questions:
|
||||
|
||||
> **Trend for `<slug>` (last 5 runs): 24 → 28 → 32 → 29 → 32**
|
||||
> Wrote `.impeccable/critique/<filename>`.
|
||||
|
||||
If this is the first run for the slug, the trend is just one score; say so: "First run for this target, no trend yet."
|
||||
|
||||
This is fire-and-forget. Do not show the user the helper's JSON output; only the human-readable trend line and the written path. Failures here should not block the rest of the flow; print the error and move on.
|
||||
|
||||
### Ask the User
|
||||
|
||||
**After presenting findings**, use targeted questions based on what was actually found. ask the user directly to clarify what you cannot infer. These answers will shape the action plan.
|
||||
|
||||
@@ -35,7 +35,14 @@ Understand the current state and goals before touching anything:
|
||||
- Loading and transition smoothness
|
||||
- Information architecture and flow drift (does this feature reveal complexity the way neighboring features do?)
|
||||
|
||||
4. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent; never perfect one corner while leaving another rough.
|
||||
4. **Pull in any prior critique** (optional signal): If `/impeccable critique` has been run on the same target, its priority issues are a useful prior for what to address first. Resolve the target to a file path or URL, then:
|
||||
```bash
|
||||
slug=$(node .qoder/skills/impeccable/scripts/critique-storage.mjs slug "<resolved>")
|
||||
node .qoder/skills/impeccable/scripts/critique-storage.mjs latest "$slug"
|
||||
```
|
||||
Exit 0 with body = found; fold the P0/P1 items into your polish list and mention the snapshot path so the user sees what you read. Exit 2 = no snapshot, continue without it. The critique is one input among many. Do your own pass either way.
|
||||
|
||||
5. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent; never perfect one corner while leaving another rough.
|
||||
|
||||
**CRITICAL**: Polish is the last step, not the first. Don't polish work that's not functionally complete.
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ After the discovery interview, generate a small set of visual direction probes *
|
||||
- The requested fidelity is **mid-fi, high-fi, or production-ready**. Skip for sketch-only planning.
|
||||
- The current harness gives you native image generation (Codex's `image_gen`, an equivalent MCP tool, or similar). Don't ask the user to install APIs or tooling.
|
||||
|
||||
When those conditions are met, this step is mandatory. If image generation isn't natively available, skip silently and proceed; don't announce the skip.
|
||||
When those conditions are met, this step is mandatory. If image generation isn't natively available, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed. The one-line announcement is required, not optional; it forces a conscious decision instead of letting the step quietly evaporate.
|
||||
|
||||
Use probes to explore visual lanes, not to replace the brief.
|
||||
|
||||
@@ -107,7 +107,7 @@ The probes should differ in primary visual direction (hierarchy, topology, densi
|
||||
- Do **not** treat generated imagery as final UX specification, final copy, or final accessibility behavior.
|
||||
- Do **not** use this step for minor refinements of existing work. It's for shaping a new surface or clarifying a big directional choice.
|
||||
|
||||
If image generation isn't natively available, skip this phase silently and proceed.
|
||||
If image generation isn't natively available, announce the skip in one line and proceed to the design brief.
|
||||
|
||||
## Phase 2: Design Brief
|
||||
|
||||
@@ -120,7 +120,7 @@ After the interview and any required probes, present a brief and **end your resp
|
||||
|
||||
Don't pad a clear brief into a long one to look thorough. A 70-line brief restating answers the user just gave is noise, not rigor. Equally, don't skip the confirmation pause to look efficient: the pause is the point.
|
||||
|
||||
If the user already said "approved" or "go" during discovery for the *exact direction you'd present*, that counts as confirmation; you may proceed without asking again. But if your brief adds anything the user hasn't seen and approved, you must stop and confirm.
|
||||
Present the brief, then **stop and wait for explicit confirmation**. You are not the judge of whether the user already approved. Even when the brief feels obviously right, ask once and wait. The pause is what separates shape from premature implementation.
|
||||
|
||||
### Brief Structure
|
||||
|
||||
@@ -158,8 +158,8 @@ Anything genuinely unresolved. Don't list "open questions" you've already recomm
|
||||
|
||||
---
|
||||
|
||||
ask the user directly to clarify what you cannot infer.
|
||||
ask the user directly to clarify what you cannot infer. Ask for explicit confirmation of the brief before finishing.
|
||||
|
||||
If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the user confirms direction (or the user already gave a clear go during discovery, which counts).
|
||||
If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the user confirms direction.
|
||||
|
||||
Once confirmed, the brief is complete. The user can now hand it to /impeccable, or use it to guide any other implementation approach. (If the user wants the full discovery-then-build flow in one step, they should use /impeccable craft instead, which runs this command internally.)
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Critique persistence helper.
|
||||
*
|
||||
* Each run of /impeccable critique writes a per-target snapshot to
|
||||
* .impeccable/critique/<timestamp>__<slug>.md
|
||||
* with a small YAML frontmatter carrying the score + P0/P1 counts.
|
||||
*
|
||||
* /impeccable polish reads the latest matching snapshot at start as its
|
||||
* fix backlog. No other skill auto-reads critique output.
|
||||
*
|
||||
* The slug is derived mechanically from the *resolved* primary artifact
|
||||
* (file path or URL), never from the user's natural-language phrasing.
|
||||
* Slug stability across runs is what lets the trend display work.
|
||||
*
|
||||
* CLI entry points (called from skill instructions):
|
||||
* node critique-storage.mjs slug <resolved-target>
|
||||
* node critique-storage.mjs write <slug> <snapshot-body-file>
|
||||
* node critique-storage.mjs latest <slug>
|
||||
* node critique-storage.mjs trend <slug> [limit]
|
||||
*
|
||||
* Note: there is intentionally no `ignore` subcommand. ignore.md is a plain
|
||||
* markdown file; the model reads it directly with its file-read tool. This
|
||||
* helper only exists for operations the model can't trivially do inline
|
||||
* (normalizing paths, generating filenames, globbing + parsing frontmatter).
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { getCritiqueDir } from './impeccable-paths.mjs';
|
||||
|
||||
const SLUG_MAX = 50;
|
||||
|
||||
/**
|
||||
* Mechanically derive a slug from a resolved target. Returns null if the
|
||||
* input doesn't look like a stable identifier (empty, project root, etc).
|
||||
*
|
||||
* Accepts file paths and URLs. The model resolves "the homepage" to a
|
||||
* concrete artifact before calling this — we never slug a natural-language
|
||||
* phrase.
|
||||
*/
|
||||
export function slugFromTarget(resolved, { cwd = process.cwd() } = {}) {
|
||||
if (!resolved || typeof resolved !== 'string') return null;
|
||||
const trimmed = resolved.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
// URL
|
||||
if (/^https?:\/\//i.test(trimmed)) {
|
||||
let url;
|
||||
try { url = new URL(trimmed); } catch { return null; }
|
||||
const hostPath = `${url.hostname}${url.pathname}`;
|
||||
return kebab(hostPath);
|
||||
}
|
||||
|
||||
// File path. Make it project-relative so two devs critiquing the same
|
||||
// checkout get the same slug regardless of where their repo is cloned.
|
||||
const abs = path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
|
||||
let rel = path.relative(cwd, abs);
|
||||
// If the target is outside cwd, fall back to the basename so we still
|
||||
// produce a stable slug (vs the absolute path, which would include
|
||||
// home dirs / usernames).
|
||||
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
||||
rel = path.basename(abs);
|
||||
}
|
||||
if (!rel || rel === '.' || rel === '') return null;
|
||||
return kebab(rel);
|
||||
}
|
||||
|
||||
function kebab(s) {
|
||||
const slug = s
|
||||
.toLowerCase()
|
||||
.replace(/[/\\.]+/g, '-')
|
||||
.replace(/[^a-z0-9-]+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
if (!slug) return null;
|
||||
// Cap from the tail — the tail (filename) is more identifying than the
|
||||
// top-level directory.
|
||||
return slug.length <= SLUG_MAX ? slug : slug.slice(slug.length - SLUG_MAX).replace(/^-/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filename-safe UTC ISO timestamp: hyphens for separators, trailing Z.
|
||||
* Plain colons aren't allowed on Windows filesystems.
|
||||
*/
|
||||
export function nowFilenameStamp(date = new Date()) {
|
||||
const iso = date.toISOString(); // 2026-05-12T18:30:00.123Z
|
||||
return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z');
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a snapshot for `slug`. `meta` carries the small structured frontmatter
|
||||
* keys read back by readTrend(). `body` is the human-readable critique
|
||||
* report (everything below the frontmatter).
|
||||
*
|
||||
* Returns the absolute path written.
|
||||
*/
|
||||
export function writeSnapshot({ slug, meta, body, cwd = process.cwd(), now = new Date() }) {
|
||||
if (!slug) throw new Error('writeSnapshot requires a slug');
|
||||
const dir = getCritiqueDir(cwd);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const timestamp = nowFilenameStamp(now);
|
||||
const filePath = path.join(dir, `${timestamp}__${slug}.md`);
|
||||
// Spread `meta` first so internally computed `timestamp` and `slug`
|
||||
// always win. Otherwise a caller-supplied meta blob (parsed from the
|
||||
// IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the
|
||||
// filename in disagreement with its frontmatter and corrupting trends.
|
||||
const front = serializeFrontmatter({ ...meta, timestamp, slug });
|
||||
fs.writeFileSync(filePath, `${front}\n${body.trim()}\n`, 'utf-8');
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function serializeFrontmatter(obj) {
|
||||
const lines = ['---'];
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
const str = typeof value === 'string' ? value : String(value);
|
||||
// Quote strings that contain : or # to keep parsing simple.
|
||||
const needsQuotes = typeof value === 'string' && /[:#]/.test(str);
|
||||
lines.push(`${key}: ${needsQuotes ? JSON.stringify(str) : str}`);
|
||||
}
|
||||
lines.push('---');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function parseFrontmatter(text) {
|
||||
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
if (!match) return {};
|
||||
const out = {};
|
||||
for (const line of match[1].split(/\r?\n/)) {
|
||||
const colon = line.indexOf(':');
|
||||
if (colon < 0) continue;
|
||||
const key = line.slice(0, colon).trim();
|
||||
let value = line.slice(colon + 1).trim();
|
||||
if (/^".*"$/.test(value)) {
|
||||
try { value = JSON.parse(value); } catch { /* leave as-is */ }
|
||||
} else if (/^-?\d+$/.test(value)) {
|
||||
value = Number(value);
|
||||
}
|
||||
out[key] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all snapshot files for `slug`, sorted oldest → newest.
|
||||
*/
|
||||
function listSnapshotsForSlug(slug, cwd) {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const suffix = `__${slug}.md`;
|
||||
return fs.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(suffix))
|
||||
.sort()
|
||||
.map((f) => path.join(dir, f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent snapshot for `slug`, or null. Polish reads this
|
||||
* to find its fix backlog when the slug matches.
|
||||
*/
|
||||
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
if (!all.length) return null;
|
||||
const latest = all[all.length - 1];
|
||||
const body = fs.readFileSync(latest, 'utf-8');
|
||||
return { path: latest, body, meta: parseFrontmatter(body) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last `limit` snapshots' frontmatter, oldest → newest.
|
||||
* Critique appends a one-line trend to its output using this.
|
||||
*/
|
||||
export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
const slice = all.slice(-limit);
|
||||
return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8')));
|
||||
}
|
||||
|
||||
// ---- CLI ---------------------------------------------------------------
|
||||
|
||||
function main(argv) {
|
||||
const [cmd, ...args] = argv;
|
||||
switch (cmd) {
|
||||
case 'slug': {
|
||||
const slug = slugFromTarget(args[0]);
|
||||
if (!slug) { process.stderr.write('no stable slug for input\n'); process.exit(1); }
|
||||
process.stdout.write(`${slug}\n`);
|
||||
return;
|
||||
}
|
||||
case 'write': {
|
||||
const [slug, bodyFile] = args;
|
||||
if (!slug || !bodyFile) { process.stderr.write('usage: write <slug> <body-file>\n'); process.exit(1); }
|
||||
const raw = fs.readFileSync(bodyFile, 'utf-8');
|
||||
// The body file may be a full report. The caller passes the meta as
|
||||
// a JSON object on stdin if it wants structured frontmatter; otherwise
|
||||
// we write with minimal metadata.
|
||||
let meta = {};
|
||||
const metaArg = process.env.IMPECCABLE_CRITIQUE_META;
|
||||
if (metaArg) {
|
||||
try { meta = JSON.parse(metaArg); } catch { /* ignore */ }
|
||||
}
|
||||
const out = writeSnapshot({ slug, meta, body: raw });
|
||||
process.stdout.write(`${out}\n`);
|
||||
return;
|
||||
}
|
||||
case 'latest': {
|
||||
const latest = readLatestSnapshot(args[0]);
|
||||
if (!latest) { process.exit(2); }
|
||||
process.stdout.write(latest.body);
|
||||
return;
|
||||
}
|
||||
case 'trend': {
|
||||
const rows = readTrend(args[0], { limit: args[1] ? Number(args[1]) : 5 });
|
||||
process.stdout.write(JSON.stringify(rows, null, 2) + '\n');
|
||||
return;
|
||||
}
|
||||
default:
|
||||
process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend> [args]\n');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main(process.argv.slice(2));
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import path from 'node:path';
|
||||
|
||||
export const IMPECCABLE_DIR = '.impeccable';
|
||||
export const LIVE_DIR = 'live';
|
||||
export const CRITIQUE_DIR = 'critique';
|
||||
|
||||
export function getImpeccableDir(cwd = process.cwd()) {
|
||||
return path.join(cwd, IMPECCABLE_DIR);
|
||||
@@ -96,6 +97,10 @@ export function getLiveAnnotationsDir(cwd = process.cwd()) {
|
||||
return path.join(getLiveDir(cwd), 'annotations');
|
||||
}
|
||||
|
||||
export function getCritiqueDir(cwd = process.cwd()) {
|
||||
return path.join(getImpeccableDir(cwd), CRITIQUE_DIR);
|
||||
}
|
||||
|
||||
export function getLegacyLiveAnnotationsDir(cwd = process.cwd()) {
|
||||
return path.join(cwd, '.impeccable-live', 'annotations');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 3.0.7
|
||||
version: 3.1.0
|
||||
user-invocable: true
|
||||
argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · teach|document|extract|live] [target]"
|
||||
license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution.
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# Codex: Visual Direction & Asset Production
|
||||
|
||||
This file is loaded by `/impeccable craft` when the harness has native image generation (currently Codex via `image_gen`). Other harnesses skip it. It covers the two craft steps that depend on real image generation: landing the visual direction, and producing the raster assets the implementation will compose.
|
||||
|
||||
Read this *before* generating any images. The order matters, and the per-step user pauses are what keep generated imagery from drifting away from the brief.
|
||||
|
||||
## Step A: Explore Directions with the User
|
||||
|
||||
Before generating anything, run a brief direction conversation grounded in the shape brief.
|
||||
|
||||
Ask **2-3 targeted questions** about visual lane, color strategy, atmosphere, and named anchor references. Don't enumerate generic menus; tie each question to the shape brief's answers. Example shape-grounded questions:
|
||||
|
||||
- "Brief says 'editorial restraint, Klim-adjacent.' Are we closer to a quiet specimen page or a magazine-spread feel with hero imagery?"
|
||||
- "Palette strategy from shape was 'Committed.' Want it warm-grounded (deep oxblood + cream) or cool-grounded (slate + paper white)?"
|
||||
|
||||
Stop and wait for answers. These pin the palette before any pixel gets generated.
|
||||
|
||||
## Step B: Generate the Brand Palette First
|
||||
|
||||
Generate **one** palette artifact before any mocks. This is a small, focused image: typography pairing on the chosen background, primary + accent color swatches, one signature ornament or motif. Single image, single pass.
|
||||
|
||||
Why palette first: mocks generated against a vague color sense produce noise that drowns out the structural decisions. A confirmed palette is the first concrete contract for everything downstream.
|
||||
|
||||
Show the palette to the user. Ask one question: "This is the palette I'm locking in for the mocks. Confirm, or call out what to shift?"
|
||||
|
||||
Wait for confirmation. Do not generate mocks against an unconfirmed palette.
|
||||
|
||||
## Step C: Generate 1-3 Visual Mocks Against the Palette
|
||||
|
||||
Once the palette is confirmed, generate **1 to 3** high-fidelity north-star comps. Each mock must use the confirmed palette and typography. Mocks differ in *structural* direction (hierarchy, topology, density, composition), not in color or motif.
|
||||
|
||||
- Brand work: push visual identity, composition, mood, and signature motifs.
|
||||
- Product work: push hierarchy, topology, density, tone, grounded in realistic product structure.
|
||||
- Landing pages and long-form brand surfaces: show enough of the second fold to establish the system beyond the hero.
|
||||
|
||||
Use the `image_gen` tool directly (or via the imagegen skill when available). Don't ask the user to install anything.
|
||||
|
||||
## Step D: Approval Loop
|
||||
|
||||
Show the comps. Ask what carries forward. Iterate until **one direction is approved** or the user explicitly delegates.
|
||||
|
||||
If the user delegates, pick the strongest direction and explain it from the brief, not personal taste.
|
||||
|
||||
Before moving to assets, summarize what to carry into code and what *not* to literalize from the mock. This is the handoff between visual exploration and semantic implementation.
|
||||
|
||||
## Step E: Mock Fidelity Inventory
|
||||
|
||||
Inventory the approved mock's major visible ingredients. For each, decide implementation: semantic HTML/CSS/SVG, generated raster, sourced raster, icon library, canvas/WebGL, or accepted omission.
|
||||
|
||||
Common ingredients to inventory:
|
||||
|
||||
- Hero silhouette and dominant composition
|
||||
- Signature motifs (planets, devices, portraits, charts, route lines, insets, badges, etc.)
|
||||
- Nav and primary CTA treatment
|
||||
- Section sequence, especially the second fold
|
||||
- Image-native content the concept depends on
|
||||
- Typography, density, color/material treatment, motion cues
|
||||
|
||||
Treat the mock as a north star, not a screenshot to trace. Don't rasterize core UI text. But if the live result lacks the mock's major ingredients, the implementation is wrong.
|
||||
|
||||
If a photographic, architectural, product, or place-led mock becomes generic CSS scenery, decorative diagrams, bullets, or copy, stop and fix it. That's a broken implementation, not a harmless interpretation.
|
||||
|
||||
Don't substitute a different hero composition or visual driver post-approval without user sign-off.
|
||||
|
||||
## Step F: Asset Slicing via the Asset Producer
|
||||
|
||||
Raster ingredients identified in Step E need clean production assets. Use the bundled `impeccable_asset_producer` subagent rather than producing inline.
|
||||
|
||||
Spawn it as a scoped subagent. If you do not have explicit permission to use agents, stop and ask:
|
||||
|
||||
```text
|
||||
Asset production will work better as a scoped subagent job. Should I spawn the Impeccable asset producer subagent for this step?
|
||||
```
|
||||
|
||||
Pass to the agent:
|
||||
|
||||
- Approved mock path or screenshot reference
|
||||
- Crop paths or a contact sheet with crop ids
|
||||
- Output directory
|
||||
- Required dimensions, format, transparency needs
|
||||
- Avoid list
|
||||
- Notes on what should remain semantic HTML/CSS/SVG instead of raster
|
||||
|
||||
Attach image generation capability to the spawned agent when the harness supports it. Do **not** load image-generation reference material into the parent thread.
|
||||
|
||||
Inline asset production is allowed only if the user declines subagents, the harness cannot spawn the authorized agent, or the user explicitly asks for single-thread mode.
|
||||
|
||||
Prefer HTML/CSS/SVG/canvas when they can credibly reproduce an ingredient; reach for real, generated, or stock imagery when the mock or subject matter calls for actual visual content.
|
||||
|
||||
## After This File
|
||||
|
||||
Once Steps A through F are complete, return to `craft.md` Step 5 (Build to Production Quality). The implementation builds against the confirmed palette, approved mock, and the assets the producer wrote.
|
||||
@@ -51,64 +51,15 @@ Then add references based on the brief's needs:
|
||||
- Responsive requirements? Consult [responsive-design.md](responsive-design.md)
|
||||
- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.md)
|
||||
|
||||
## Step 3: Land the Visual Direction (Capability-Gated)
|
||||
## Step 3: Visual Direction & Assets (Harness-Gated)
|
||||
|
||||
Generate high-fidelity visual comps before implementation when all three are true:
|
||||
If the harness has **native image generation** (currently Codex via `image_gen`), this step is mandatory. **Stop and load [codex.md](codex.md)**. It covers palette generation, mock exploration, the approval loop, mock-fidelity inventory, and asset slicing via the `impeccable_asset_producer` subagent. Follow Steps A-F in that file, then return here for Step 4.
|
||||
|
||||
- The work is net-new or visually open-ended enough that composition exploration will improve the build.
|
||||
- The brief's scope is mid-fi, high-fi, or production-ready.
|
||||
- The harness gives you native image generation (Codex's `image_gen`, an equivalent MCP tool). Don't ask the user to set up external APIs.
|
||||
If the harness lacks native image generation, **state in one line that the visual-direction-by-generation step is being skipped because the harness lacks native image generation, then proceed**. The one-line announcement is required; it forces a conscious decision instead of letting the step quietly evaporate. The brief is your only visual reference. Implement directly from it, treating any named anchor references and the brief's "Design Direction" as the contract.
|
||||
|
||||
When met, this step is mandatory for both brand and product work. If image generation isn't available, skip silently. "The eventual UI is semantic/code-native/accessible" is not a reason to skip; those are implementation requirements, not exploration ones.
|
||||
Whether you generated mocks or not: don't replace required imagery with generic cards, bullets, emoji, fake metrics, decorative CSS panels, or filler copy. Image-led briefs (restaurants, hotels, magazines, photography, hobbyist communities, food, travel, fashion, product) need real or sourced imagery in the build, not CSS scenery.
|
||||
|
||||
### What to generate
|
||||
|
||||
Generate **1 to 3** high-fidelity north-star comps from the confirmed brief. If shape already produced direction probes, resolve the winning lane further, not an unrelated exploration. Comps must differ in primary visual direction, not just color.
|
||||
|
||||
- Brand work: push visual identity, composition, and mood aggressively.
|
||||
- Product work: push hierarchy, topology, density, tone, grounded in realistic product structure.
|
||||
- Landing pages and long-form brand surfaces: show enough of the second fold to establish the system beyond the hero.
|
||||
|
||||
### Approval loop
|
||||
|
||||
Show the comps and ask what carries forward. Iterate until one direction is approved or the user delegates. If the user delegates, pick the strongest direction and explain it from the brief, not personal taste.
|
||||
|
||||
Before Step 4, summarize what to carry into code and what **not** to literalize from the mock. This is the handoff between visual exploration and semantic implementation.
|
||||
|
||||
### Mock fidelity inventory
|
||||
|
||||
Inventory the approved mock's major visible ingredients:
|
||||
|
||||
- Hero silhouette and dominant composition
|
||||
- Signature motifs (planets, devices, portraits, charts, route lines, insets, badges, etc.)
|
||||
- Nav and primary CTA treatment
|
||||
- Section sequence, especially the second fold
|
||||
- Image-native content the concept depends on
|
||||
- Typography, density, color/material treatment, motion cues
|
||||
|
||||
For each, decide implementation: semantic HTML/CSS/SVG, generated asset, sourced asset, icon library, canvas/WebGL, or accepted omission. Don't substitute a different hero composition or visual driver post-approval without user sign-off.
|
||||
|
||||
If a photographic, architectural, product, or place-led mock becomes generic CSS scenery / decorative diagrams / bullets / copy, stop and fix it. That's a broken implementation, not a harmless interpretation.
|
||||
|
||||
Treat the mock as a north star, not a screenshot to trace. Don't rasterize core UI text. But if the live result lacks the mock's major ingredients, the implementation is wrong.
|
||||
|
||||
## Step 4: Asset Extraction (Need-Gated)
|
||||
|
||||
If the approved direction needs raster assets, create them before building. Do not replace required imagery with generic cards, bullets, emoji, fake metrics, decorative CSS panels, or filler copy.
|
||||
|
||||
Use the native asset producer (`impeccable_asset_producer` in Codex, `impeccable-asset-producer` in Claude Code) to create clean assets from the hi-fi mock and crops. If you do not have explicit permission to use agents, stop and ask:
|
||||
|
||||
```text
|
||||
Asset production will work better as a scoped subagent job. Should I spawn the Impeccable asset producer subagent for this step?
|
||||
```
|
||||
|
||||
Do not skip asset production or silently do it inline. Inline asset production is allowed only if the user declines subagents, the harness cannot spawn the authorized agent, or the user explicitly asks for single-thread mode.
|
||||
|
||||
Pass the approved mock, crop/contact-sheet paths, output directory, dimensions/formats, transparency needs, constraints, and avoid list to the asset producer. Attach image generation capability to the spawned agent when the harness supports it; do not load image-generation reference material into the parent thread first.
|
||||
|
||||
Prefer HTML/CSS/SVG/canvas when they can credibly reproduce an ingredient; use real/generated/stock imagery when the mock or subject matter calls for actual visual content.
|
||||
|
||||
## Step 5: Build to Production Quality
|
||||
## Step 4: Build to Production Quality
|
||||
|
||||
Implement the feature following the design brief. Build in passes so structure, visual system, states, motion/media, and responsive behavior each get deliberate attention. The list below is the definition of done, not inspiration.
|
||||
|
||||
@@ -130,7 +81,7 @@ Implement the feature following the design brief. Build in passes so structure,
|
||||
- **Technically clean.** Production build passes, no console errors, no avoidable layout shift, no needless dependencies, no broken asset paths.
|
||||
- **Ask when uncertain.** If a discovery materially changes the brief or approved direction, stop and ask. Don't guess.
|
||||
|
||||
## Step 6: Iterate Visually
|
||||
## Step 5: Iterate Visually
|
||||
|
||||
Look at what you built like a designer would. Your eyes are whatever the harness gives you: a connected browser, a screenshotting tool, Playwright, or asking the user. Use them for responsive testing (mobile, tablet, desktop minimum) and general visual validation.
|
||||
|
||||
@@ -144,7 +95,7 @@ Actively check: responsive behavior (composes, not shrinks), every state (empty
|
||||
|
||||
Detector or QA output is defect evidence only; never proof the work is finished.
|
||||
|
||||
## Step 7: Present
|
||||
## Step 6: Present
|
||||
|
||||
Present the result to the user:
|
||||
- Show the feature in its primary state
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
> **Additional context needed**: what the interface is trying to accomplish.
|
||||
|
||||
### Setup: Resolve Target and Load Ignore List
|
||||
|
||||
Before gathering assessments, do two small bookkeeping steps. They cost almost nothing and they're what makes critique iterative across runs.
|
||||
|
||||
1. **Resolve the primary artifact.** The user's phrasing ("the homepage", "the pricing flow") is not stable enough to track across runs. Resolve it to a concrete file path or URL: the same one you'd already need to scan code or open in a browser. Examples:
|
||||
- "the homepage" → `site/pages/index.astro` (or `http://localhost:3000/` if you're inspecting live)
|
||||
- "the settings modal" → the primary component file (e.g., `src/components/Settings.tsx`)
|
||||
- "this page" → the URL or the page's source file
|
||||
Prefer the source file path over the dev-server URL when both exist; ports drift between runs (`bun dev` vs `bun preview`), file paths don't.
|
||||
|
||||
2. **Compute the slug.** Run:
|
||||
```bash
|
||||
node .rovodev/skills/impeccable/scripts/critique-storage.mjs slug "<resolved-path-or-url>"
|
||||
```
|
||||
Keep the printed slug. It identifies this target's stream across runs. If the command exits non-zero ("no stable slug for input"), skip persistence for this run and tell the user; the trend won't update but the critique still goes ahead.
|
||||
|
||||
3. **Read the ignore list** at `.impeccable/critique/ignore.md` if it exists. Plain markdown; each non-empty, non-comment line is something the user has marked as "do not re-raise" (deferred tradeoffs, designer-intended deviations, detector false-positives the user accepts). When a finding's text matches a line here (case-insensitive substring against rule name or snippet), **drop it silently**. Do not mention it in the report. This is the ONLY input critique consumes from prior runs; anchoring on prior findings would defeat the point of independent assessment.
|
||||
|
||||
### Gather Assessments
|
||||
|
||||
Launch two independent assessments. **Neither may see the other's output.** This isolation is what makes the combined score honest. Running both in one head silently anchors them to each other; do not shortcut it for cost, speed, or context-size reasons.
|
||||
@@ -164,6 +182,36 @@ Provocative questions that might unlock better solutions:
|
||||
- Prioritize ruthlessly. If everything is important, nothing is.
|
||||
- Don't soften criticism. Developers need honest feedback to ship great design.
|
||||
|
||||
### Persist the Snapshot
|
||||
|
||||
Once the report above is finalized, write it to `.impeccable/critique/` so the user can refer back, and so `/impeccable polish` can pick up the priority issues without a copy-paste.
|
||||
|
||||
Skip this step if the Setup slug was null (vague or root-level target).
|
||||
|
||||
1. **Write the body to a temp file** so you can pipe it to the helper. Use the full report (heuristic table, anti-patterns verdict, priority issues, persona red flags) but stop before the "Ask the User" / "Recommended Actions" sections that come later.
|
||||
|
||||
2. **Pass the structured metadata** through `IMPECCABLE_CRITIQUE_META` (JSON), then run the write command:
|
||||
```bash
|
||||
IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"p0_count":<n>,"p1_count":<n>}' \
|
||||
node .rovodev/skills/impeccable/scripts/critique-storage.mjs write <slug> <body-file>
|
||||
```
|
||||
The helper prints the absolute path it wrote.
|
||||
|
||||
3. **Read the trend** for context:
|
||||
```bash
|
||||
node .rovodev/skills/impeccable/scripts/critique-storage.mjs trend <slug> 5
|
||||
```
|
||||
This returns a JSON array of the last 5 frontmatter entries (including the one you just wrote).
|
||||
|
||||
4. **Append a single line to the user-visible output**, after the report and before the questions:
|
||||
|
||||
> **Trend for `<slug>` (last 5 runs): 24 → 28 → 32 → 29 → 32**
|
||||
> Wrote `.impeccable/critique/<filename>`.
|
||||
|
||||
If this is the first run for the slug, the trend is just one score; say so: "First run for this target, no trend yet."
|
||||
|
||||
This is fire-and-forget. Do not show the user the helper's JSON output; only the human-readable trend line and the written path. Failures here should not block the rest of the flow; print the error and move on.
|
||||
|
||||
### Ask the User
|
||||
|
||||
**After presenting findings**, use targeted questions based on what was actually found. ask the user directly to clarify what you cannot infer. These answers will shape the action plan.
|
||||
|
||||
@@ -35,7 +35,14 @@ Understand the current state and goals before touching anything:
|
||||
- Loading and transition smoothness
|
||||
- Information architecture and flow drift (does this feature reveal complexity the way neighboring features do?)
|
||||
|
||||
4. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent; never perfect one corner while leaving another rough.
|
||||
4. **Pull in any prior critique** (optional signal): If `/impeccable critique` has been run on the same target, its priority issues are a useful prior for what to address first. Resolve the target to a file path or URL, then:
|
||||
```bash
|
||||
slug=$(node .rovodev/skills/impeccable/scripts/critique-storage.mjs slug "<resolved>")
|
||||
node .rovodev/skills/impeccable/scripts/critique-storage.mjs latest "$slug"
|
||||
```
|
||||
Exit 0 with body = found; fold the P0/P1 items into your polish list and mention the snapshot path so the user sees what you read. Exit 2 = no snapshot, continue without it. The critique is one input among many. Do your own pass either way.
|
||||
|
||||
5. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent; never perfect one corner while leaving another rough.
|
||||
|
||||
**CRITICAL**: Polish is the last step, not the first. Don't polish work that's not functionally complete.
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ After the discovery interview, generate a small set of visual direction probes *
|
||||
- The requested fidelity is **mid-fi, high-fi, or production-ready**. Skip for sketch-only planning.
|
||||
- The current harness gives you native image generation (Codex's `image_gen`, an equivalent MCP tool, or similar). Don't ask the user to install APIs or tooling.
|
||||
|
||||
When those conditions are met, this step is mandatory. If image generation isn't natively available, skip silently and proceed; don't announce the skip.
|
||||
When those conditions are met, this step is mandatory. If image generation isn't natively available, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed. The one-line announcement is required, not optional; it forces a conscious decision instead of letting the step quietly evaporate.
|
||||
|
||||
Use probes to explore visual lanes, not to replace the brief.
|
||||
|
||||
@@ -107,7 +107,7 @@ The probes should differ in primary visual direction (hierarchy, topology, densi
|
||||
- Do **not** treat generated imagery as final UX specification, final copy, or final accessibility behavior.
|
||||
- Do **not** use this step for minor refinements of existing work. It's for shaping a new surface or clarifying a big directional choice.
|
||||
|
||||
If image generation isn't natively available, skip this phase silently and proceed.
|
||||
If image generation isn't natively available, announce the skip in one line and proceed to the design brief.
|
||||
|
||||
## Phase 2: Design Brief
|
||||
|
||||
@@ -120,7 +120,7 @@ After the interview and any required probes, present a brief and **end your resp
|
||||
|
||||
Don't pad a clear brief into a long one to look thorough. A 70-line brief restating answers the user just gave is noise, not rigor. Equally, don't skip the confirmation pause to look efficient: the pause is the point.
|
||||
|
||||
If the user already said "approved" or "go" during discovery for the *exact direction you'd present*, that counts as confirmation; you may proceed without asking again. But if your brief adds anything the user hasn't seen and approved, you must stop and confirm.
|
||||
Present the brief, then **stop and wait for explicit confirmation**. You are not the judge of whether the user already approved. Even when the brief feels obviously right, ask once and wait. The pause is what separates shape from premature implementation.
|
||||
|
||||
### Brief Structure
|
||||
|
||||
@@ -158,8 +158,8 @@ Anything genuinely unresolved. Don't list "open questions" you've already recomm
|
||||
|
||||
---
|
||||
|
||||
ask the user directly to clarify what you cannot infer.
|
||||
ask the user directly to clarify what you cannot infer. Ask for explicit confirmation of the brief before finishing.
|
||||
|
||||
If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the user confirms direction (or the user already gave a clear go during discovery, which counts).
|
||||
If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the user confirms direction.
|
||||
|
||||
Once confirmed, the brief is complete. The user can now hand it to /impeccable, or use it to guide any other implementation approach. (If the user wants the full discovery-then-build flow in one step, they should use /impeccable craft instead, which runs this command internally.)
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Critique persistence helper.
|
||||
*
|
||||
* Each run of /impeccable critique writes a per-target snapshot to
|
||||
* .impeccable/critique/<timestamp>__<slug>.md
|
||||
* with a small YAML frontmatter carrying the score + P0/P1 counts.
|
||||
*
|
||||
* /impeccable polish reads the latest matching snapshot at start as its
|
||||
* fix backlog. No other skill auto-reads critique output.
|
||||
*
|
||||
* The slug is derived mechanically from the *resolved* primary artifact
|
||||
* (file path or URL), never from the user's natural-language phrasing.
|
||||
* Slug stability across runs is what lets the trend display work.
|
||||
*
|
||||
* CLI entry points (called from skill instructions):
|
||||
* node critique-storage.mjs slug <resolved-target>
|
||||
* node critique-storage.mjs write <slug> <snapshot-body-file>
|
||||
* node critique-storage.mjs latest <slug>
|
||||
* node critique-storage.mjs trend <slug> [limit]
|
||||
*
|
||||
* Note: there is intentionally no `ignore` subcommand. ignore.md is a plain
|
||||
* markdown file; the model reads it directly with its file-read tool. This
|
||||
* helper only exists for operations the model can't trivially do inline
|
||||
* (normalizing paths, generating filenames, globbing + parsing frontmatter).
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { getCritiqueDir } from './impeccable-paths.mjs';
|
||||
|
||||
const SLUG_MAX = 50;
|
||||
|
||||
/**
|
||||
* Mechanically derive a slug from a resolved target. Returns null if the
|
||||
* input doesn't look like a stable identifier (empty, project root, etc).
|
||||
*
|
||||
* Accepts file paths and URLs. The model resolves "the homepage" to a
|
||||
* concrete artifact before calling this — we never slug a natural-language
|
||||
* phrase.
|
||||
*/
|
||||
export function slugFromTarget(resolved, { cwd = process.cwd() } = {}) {
|
||||
if (!resolved || typeof resolved !== 'string') return null;
|
||||
const trimmed = resolved.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
// URL
|
||||
if (/^https?:\/\//i.test(trimmed)) {
|
||||
let url;
|
||||
try { url = new URL(trimmed); } catch { return null; }
|
||||
const hostPath = `${url.hostname}${url.pathname}`;
|
||||
return kebab(hostPath);
|
||||
}
|
||||
|
||||
// File path. Make it project-relative so two devs critiquing the same
|
||||
// checkout get the same slug regardless of where their repo is cloned.
|
||||
const abs = path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
|
||||
let rel = path.relative(cwd, abs);
|
||||
// If the target is outside cwd, fall back to the basename so we still
|
||||
// produce a stable slug (vs the absolute path, which would include
|
||||
// home dirs / usernames).
|
||||
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
||||
rel = path.basename(abs);
|
||||
}
|
||||
if (!rel || rel === '.' || rel === '') return null;
|
||||
return kebab(rel);
|
||||
}
|
||||
|
||||
function kebab(s) {
|
||||
const slug = s
|
||||
.toLowerCase()
|
||||
.replace(/[/\\.]+/g, '-')
|
||||
.replace(/[^a-z0-9-]+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
if (!slug) return null;
|
||||
// Cap from the tail — the tail (filename) is more identifying than the
|
||||
// top-level directory.
|
||||
return slug.length <= SLUG_MAX ? slug : slug.slice(slug.length - SLUG_MAX).replace(/^-/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filename-safe UTC ISO timestamp: hyphens for separators, trailing Z.
|
||||
* Plain colons aren't allowed on Windows filesystems.
|
||||
*/
|
||||
export function nowFilenameStamp(date = new Date()) {
|
||||
const iso = date.toISOString(); // 2026-05-12T18:30:00.123Z
|
||||
return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z');
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a snapshot for `slug`. `meta` carries the small structured frontmatter
|
||||
* keys read back by readTrend(). `body` is the human-readable critique
|
||||
* report (everything below the frontmatter).
|
||||
*
|
||||
* Returns the absolute path written.
|
||||
*/
|
||||
export function writeSnapshot({ slug, meta, body, cwd = process.cwd(), now = new Date() }) {
|
||||
if (!slug) throw new Error('writeSnapshot requires a slug');
|
||||
const dir = getCritiqueDir(cwd);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const timestamp = nowFilenameStamp(now);
|
||||
const filePath = path.join(dir, `${timestamp}__${slug}.md`);
|
||||
// Spread `meta` first so internally computed `timestamp` and `slug`
|
||||
// always win. Otherwise a caller-supplied meta blob (parsed from the
|
||||
// IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the
|
||||
// filename in disagreement with its frontmatter and corrupting trends.
|
||||
const front = serializeFrontmatter({ ...meta, timestamp, slug });
|
||||
fs.writeFileSync(filePath, `${front}\n${body.trim()}\n`, 'utf-8');
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function serializeFrontmatter(obj) {
|
||||
const lines = ['---'];
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
const str = typeof value === 'string' ? value : String(value);
|
||||
// Quote strings that contain : or # to keep parsing simple.
|
||||
const needsQuotes = typeof value === 'string' && /[:#]/.test(str);
|
||||
lines.push(`${key}: ${needsQuotes ? JSON.stringify(str) : str}`);
|
||||
}
|
||||
lines.push('---');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function parseFrontmatter(text) {
|
||||
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
if (!match) return {};
|
||||
const out = {};
|
||||
for (const line of match[1].split(/\r?\n/)) {
|
||||
const colon = line.indexOf(':');
|
||||
if (colon < 0) continue;
|
||||
const key = line.slice(0, colon).trim();
|
||||
let value = line.slice(colon + 1).trim();
|
||||
if (/^".*"$/.test(value)) {
|
||||
try { value = JSON.parse(value); } catch { /* leave as-is */ }
|
||||
} else if (/^-?\d+$/.test(value)) {
|
||||
value = Number(value);
|
||||
}
|
||||
out[key] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all snapshot files for `slug`, sorted oldest → newest.
|
||||
*/
|
||||
function listSnapshotsForSlug(slug, cwd) {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const suffix = `__${slug}.md`;
|
||||
return fs.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(suffix))
|
||||
.sort()
|
||||
.map((f) => path.join(dir, f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent snapshot for `slug`, or null. Polish reads this
|
||||
* to find its fix backlog when the slug matches.
|
||||
*/
|
||||
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
if (!all.length) return null;
|
||||
const latest = all[all.length - 1];
|
||||
const body = fs.readFileSync(latest, 'utf-8');
|
||||
return { path: latest, body, meta: parseFrontmatter(body) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last `limit` snapshots' frontmatter, oldest → newest.
|
||||
* Critique appends a one-line trend to its output using this.
|
||||
*/
|
||||
export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
const slice = all.slice(-limit);
|
||||
return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8')));
|
||||
}
|
||||
|
||||
// ---- CLI ---------------------------------------------------------------
|
||||
|
||||
function main(argv) {
|
||||
const [cmd, ...args] = argv;
|
||||
switch (cmd) {
|
||||
case 'slug': {
|
||||
const slug = slugFromTarget(args[0]);
|
||||
if (!slug) { process.stderr.write('no stable slug for input\n'); process.exit(1); }
|
||||
process.stdout.write(`${slug}\n`);
|
||||
return;
|
||||
}
|
||||
case 'write': {
|
||||
const [slug, bodyFile] = args;
|
||||
if (!slug || !bodyFile) { process.stderr.write('usage: write <slug> <body-file>\n'); process.exit(1); }
|
||||
const raw = fs.readFileSync(bodyFile, 'utf-8');
|
||||
// The body file may be a full report. The caller passes the meta as
|
||||
// a JSON object on stdin if it wants structured frontmatter; otherwise
|
||||
// we write with minimal metadata.
|
||||
let meta = {};
|
||||
const metaArg = process.env.IMPECCABLE_CRITIQUE_META;
|
||||
if (metaArg) {
|
||||
try { meta = JSON.parse(metaArg); } catch { /* ignore */ }
|
||||
}
|
||||
const out = writeSnapshot({ slug, meta, body: raw });
|
||||
process.stdout.write(`${out}\n`);
|
||||
return;
|
||||
}
|
||||
case 'latest': {
|
||||
const latest = readLatestSnapshot(args[0]);
|
||||
if (!latest) { process.exit(2); }
|
||||
process.stdout.write(latest.body);
|
||||
return;
|
||||
}
|
||||
case 'trend': {
|
||||
const rows = readTrend(args[0], { limit: args[1] ? Number(args[1]) : 5 });
|
||||
process.stdout.write(JSON.stringify(rows, null, 2) + '\n');
|
||||
return;
|
||||
}
|
||||
default:
|
||||
process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend> [args]\n');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main(process.argv.slice(2));
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import path from 'node:path';
|
||||
|
||||
export const IMPECCABLE_DIR = '.impeccable';
|
||||
export const LIVE_DIR = 'live';
|
||||
export const CRITIQUE_DIR = 'critique';
|
||||
|
||||
export function getImpeccableDir(cwd = process.cwd()) {
|
||||
return path.join(cwd, IMPECCABLE_DIR);
|
||||
@@ -96,6 +97,10 @@ export function getLiveAnnotationsDir(cwd = process.cwd()) {
|
||||
return path.join(getLiveDir(cwd), 'annotations');
|
||||
}
|
||||
|
||||
export function getCritiqueDir(cwd = process.cwd()) {
|
||||
return path.join(getImpeccableDir(cwd), CRITIQUE_DIR);
|
||||
}
|
||||
|
||||
export function getLegacyLiveAnnotationsDir(cwd = process.cwd()) {
|
||||
return path.join(cwd, '.impeccable-live', 'annotations');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 3.0.7
|
||||
version: 3.1.0
|
||||
user-invocable: true
|
||||
argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · teach|document|extract|live] [target]"
|
||||
license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution.
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# Codex: Visual Direction & Asset Production
|
||||
|
||||
This file is loaded by `/impeccable craft` when the harness has native image generation (currently Codex via `image_gen`). Other harnesses skip it. It covers the two craft steps that depend on real image generation: landing the visual direction, and producing the raster assets the implementation will compose.
|
||||
|
||||
Read this *before* generating any images. The order matters, and the per-step user pauses are what keep generated imagery from drifting away from the brief.
|
||||
|
||||
## Step A: Explore Directions with the User
|
||||
|
||||
Before generating anything, run a brief direction conversation grounded in the shape brief.
|
||||
|
||||
Ask **2-3 targeted questions** about visual lane, color strategy, atmosphere, and named anchor references. Don't enumerate generic menus; tie each question to the shape brief's answers. Example shape-grounded questions:
|
||||
|
||||
- "Brief says 'editorial restraint, Klim-adjacent.' Are we closer to a quiet specimen page or a magazine-spread feel with hero imagery?"
|
||||
- "Palette strategy from shape was 'Committed.' Want it warm-grounded (deep oxblood + cream) or cool-grounded (slate + paper white)?"
|
||||
|
||||
Stop and wait for answers. These pin the palette before any pixel gets generated.
|
||||
|
||||
## Step B: Generate the Brand Palette First
|
||||
|
||||
Generate **one** palette artifact before any mocks. This is a small, focused image: typography pairing on the chosen background, primary + accent color swatches, one signature ornament or motif. Single image, single pass.
|
||||
|
||||
Why palette first: mocks generated against a vague color sense produce noise that drowns out the structural decisions. A confirmed palette is the first concrete contract for everything downstream.
|
||||
|
||||
Show the palette to the user. Ask one question: "This is the palette I'm locking in for the mocks. Confirm, or call out what to shift?"
|
||||
|
||||
Wait for confirmation. Do not generate mocks against an unconfirmed palette.
|
||||
|
||||
## Step C: Generate 1-3 Visual Mocks Against the Palette
|
||||
|
||||
Once the palette is confirmed, generate **1 to 3** high-fidelity north-star comps. Each mock must use the confirmed palette and typography. Mocks differ in *structural* direction (hierarchy, topology, density, composition), not in color or motif.
|
||||
|
||||
- Brand work: push visual identity, composition, mood, and signature motifs.
|
||||
- Product work: push hierarchy, topology, density, tone, grounded in realistic product structure.
|
||||
- Landing pages and long-form brand surfaces: show enough of the second fold to establish the system beyond the hero.
|
||||
|
||||
Use the `image_gen` tool directly (or via the imagegen skill when available). Don't ask the user to install anything.
|
||||
|
||||
## Step D: Approval Loop
|
||||
|
||||
Show the comps. Ask what carries forward. Iterate until **one direction is approved** or the user explicitly delegates.
|
||||
|
||||
If the user delegates, pick the strongest direction and explain it from the brief, not personal taste.
|
||||
|
||||
Before moving to assets, summarize what to carry into code and what *not* to literalize from the mock. This is the handoff between visual exploration and semantic implementation.
|
||||
|
||||
## Step E: Mock Fidelity Inventory
|
||||
|
||||
Inventory the approved mock's major visible ingredients. For each, decide implementation: semantic HTML/CSS/SVG, generated raster, sourced raster, icon library, canvas/WebGL, or accepted omission.
|
||||
|
||||
Common ingredients to inventory:
|
||||
|
||||
- Hero silhouette and dominant composition
|
||||
- Signature motifs (planets, devices, portraits, charts, route lines, insets, badges, etc.)
|
||||
- Nav and primary CTA treatment
|
||||
- Section sequence, especially the second fold
|
||||
- Image-native content the concept depends on
|
||||
- Typography, density, color/material treatment, motion cues
|
||||
|
||||
Treat the mock as a north star, not a screenshot to trace. Don't rasterize core UI text. But if the live result lacks the mock's major ingredients, the implementation is wrong.
|
||||
|
||||
If a photographic, architectural, product, or place-led mock becomes generic CSS scenery, decorative diagrams, bullets, or copy, stop and fix it. That's a broken implementation, not a harmless interpretation.
|
||||
|
||||
Don't substitute a different hero composition or visual driver post-approval without user sign-off.
|
||||
|
||||
## Step F: Asset Slicing via the Asset Producer
|
||||
|
||||
Raster ingredients identified in Step E need clean production assets. Use the bundled `impeccable_asset_producer` subagent rather than producing inline.
|
||||
|
||||
Spawn it as a scoped subagent. If you do not have explicit permission to use agents, stop and ask:
|
||||
|
||||
```text
|
||||
Asset production will work better as a scoped subagent job. Should I spawn the Impeccable asset producer subagent for this step?
|
||||
```
|
||||
|
||||
Pass to the agent:
|
||||
|
||||
- Approved mock path or screenshot reference
|
||||
- Crop paths or a contact sheet with crop ids
|
||||
- Output directory
|
||||
- Required dimensions, format, transparency needs
|
||||
- Avoid list
|
||||
- Notes on what should remain semantic HTML/CSS/SVG instead of raster
|
||||
|
||||
Attach image generation capability to the spawned agent when the harness supports it. Do **not** load image-generation reference material into the parent thread.
|
||||
|
||||
Inline asset production is allowed only if the user declines subagents, the harness cannot spawn the authorized agent, or the user explicitly asks for single-thread mode.
|
||||
|
||||
Prefer HTML/CSS/SVG/canvas when they can credibly reproduce an ingredient; reach for real, generated, or stock imagery when the mock or subject matter calls for actual visual content.
|
||||
|
||||
## After This File
|
||||
|
||||
Once Steps A through F are complete, return to `craft.md` Step 5 (Build to Production Quality). The implementation builds against the confirmed palette, approved mock, and the assets the producer wrote.
|
||||
@@ -51,64 +51,15 @@ Then add references based on the brief's needs:
|
||||
- Responsive requirements? Consult [responsive-design.md](responsive-design.md)
|
||||
- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.md)
|
||||
|
||||
## Step 3: Land the Visual Direction (Capability-Gated)
|
||||
## Step 3: Visual Direction & Assets (Harness-Gated)
|
||||
|
||||
Generate high-fidelity visual comps before implementation when all three are true:
|
||||
If the harness has **native image generation** (currently Codex via `image_gen`), this step is mandatory. **Stop and load [codex.md](codex.md)**. It covers palette generation, mock exploration, the approval loop, mock-fidelity inventory, and asset slicing via the `impeccable_asset_producer` subagent. Follow Steps A-F in that file, then return here for Step 4.
|
||||
|
||||
- The work is net-new or visually open-ended enough that composition exploration will improve the build.
|
||||
- The brief's scope is mid-fi, high-fi, or production-ready.
|
||||
- The harness gives you native image generation (Codex's `image_gen`, an equivalent MCP tool). Don't ask the user to set up external APIs.
|
||||
If the harness lacks native image generation, **state in one line that the visual-direction-by-generation step is being skipped because the harness lacks native image generation, then proceed**. The one-line announcement is required; it forces a conscious decision instead of letting the step quietly evaporate. The brief is your only visual reference. Implement directly from it, treating any named anchor references and the brief's "Design Direction" as the contract.
|
||||
|
||||
When met, this step is mandatory for both brand and product work. If image generation isn't available, skip silently. "The eventual UI is semantic/code-native/accessible" is not a reason to skip; those are implementation requirements, not exploration ones.
|
||||
Whether you generated mocks or not: don't replace required imagery with generic cards, bullets, emoji, fake metrics, decorative CSS panels, or filler copy. Image-led briefs (restaurants, hotels, magazines, photography, hobbyist communities, food, travel, fashion, product) need real or sourced imagery in the build, not CSS scenery.
|
||||
|
||||
### What to generate
|
||||
|
||||
Generate **1 to 3** high-fidelity north-star comps from the confirmed brief. If shape already produced direction probes, resolve the winning lane further, not an unrelated exploration. Comps must differ in primary visual direction, not just color.
|
||||
|
||||
- Brand work: push visual identity, composition, and mood aggressively.
|
||||
- Product work: push hierarchy, topology, density, tone, grounded in realistic product structure.
|
||||
- Landing pages and long-form brand surfaces: show enough of the second fold to establish the system beyond the hero.
|
||||
|
||||
### Approval loop
|
||||
|
||||
Show the comps and ask what carries forward. Iterate until one direction is approved or the user delegates. If the user delegates, pick the strongest direction and explain it from the brief, not personal taste.
|
||||
|
||||
Before Step 4, summarize what to carry into code and what **not** to literalize from the mock. This is the handoff between visual exploration and semantic implementation.
|
||||
|
||||
### Mock fidelity inventory
|
||||
|
||||
Inventory the approved mock's major visible ingredients:
|
||||
|
||||
- Hero silhouette and dominant composition
|
||||
- Signature motifs (planets, devices, portraits, charts, route lines, insets, badges, etc.)
|
||||
- Nav and primary CTA treatment
|
||||
- Section sequence, especially the second fold
|
||||
- Image-native content the concept depends on
|
||||
- Typography, density, color/material treatment, motion cues
|
||||
|
||||
For each, decide implementation: semantic HTML/CSS/SVG, generated asset, sourced asset, icon library, canvas/WebGL, or accepted omission. Don't substitute a different hero composition or visual driver post-approval without user sign-off.
|
||||
|
||||
If a photographic, architectural, product, or place-led mock becomes generic CSS scenery / decorative diagrams / bullets / copy, stop and fix it. That's a broken implementation, not a harmless interpretation.
|
||||
|
||||
Treat the mock as a north star, not a screenshot to trace. Don't rasterize core UI text. But if the live result lacks the mock's major ingredients, the implementation is wrong.
|
||||
|
||||
## Step 4: Asset Extraction (Need-Gated)
|
||||
|
||||
If the approved direction needs raster assets, create them before building. Do not replace required imagery with generic cards, bullets, emoji, fake metrics, decorative CSS panels, or filler copy.
|
||||
|
||||
Use the native asset producer (`impeccable_asset_producer` in Codex, `impeccable-asset-producer` in Claude Code) to create clean assets from the hi-fi mock and crops. If you do not have explicit permission to use agents, stop and ask:
|
||||
|
||||
```text
|
||||
Asset production will work better as a scoped subagent job. Should I spawn the Impeccable asset producer subagent for this step?
|
||||
```
|
||||
|
||||
Do not skip asset production or silently do it inline. Inline asset production is allowed only if the user declines subagents, the harness cannot spawn the authorized agent, or the user explicitly asks for single-thread mode.
|
||||
|
||||
Pass the approved mock, crop/contact-sheet paths, output directory, dimensions/formats, transparency needs, constraints, and avoid list to the asset producer. Attach image generation capability to the spawned agent when the harness supports it; do not load image-generation reference material into the parent thread first.
|
||||
|
||||
Prefer HTML/CSS/SVG/canvas when they can credibly reproduce an ingredient; use real/generated/stock imagery when the mock or subject matter calls for actual visual content.
|
||||
|
||||
## Step 5: Build to Production Quality
|
||||
## Step 4: Build to Production Quality
|
||||
|
||||
Implement the feature following the design brief. Build in passes so structure, visual system, states, motion/media, and responsive behavior each get deliberate attention. The list below is the definition of done, not inspiration.
|
||||
|
||||
@@ -130,7 +81,7 @@ Implement the feature following the design brief. Build in passes so structure,
|
||||
- **Technically clean.** Production build passes, no console errors, no avoidable layout shift, no needless dependencies, no broken asset paths.
|
||||
- **Ask when uncertain.** If a discovery materially changes the brief or approved direction, stop and ask. Don't guess.
|
||||
|
||||
## Step 6: Iterate Visually
|
||||
## Step 5: Iterate Visually
|
||||
|
||||
Look at what you built like a designer would. Your eyes are whatever the harness gives you: a connected browser, a screenshotting tool, Playwright, or asking the user. Use them for responsive testing (mobile, tablet, desktop minimum) and general visual validation.
|
||||
|
||||
@@ -144,7 +95,7 @@ Actively check: responsive behavior (composes, not shrinks), every state (empty
|
||||
|
||||
Detector or QA output is defect evidence only; never proof the work is finished.
|
||||
|
||||
## Step 7: Present
|
||||
## Step 6: Present
|
||||
|
||||
Present the result to the user:
|
||||
- Show the feature in its primary state
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
> **Additional context needed**: what the interface is trying to accomplish.
|
||||
|
||||
### Setup: Resolve Target and Load Ignore List
|
||||
|
||||
Before gathering assessments, do two small bookkeeping steps. They cost almost nothing and they're what makes critique iterative across runs.
|
||||
|
||||
1. **Resolve the primary artifact.** The user's phrasing ("the homepage", "the pricing flow") is not stable enough to track across runs. Resolve it to a concrete file path or URL: the same one you'd already need to scan code or open in a browser. Examples:
|
||||
- "the homepage" → `site/pages/index.astro` (or `http://localhost:3000/` if you're inspecting live)
|
||||
- "the settings modal" → the primary component file (e.g., `src/components/Settings.tsx`)
|
||||
- "this page" → the URL or the page's source file
|
||||
Prefer the source file path over the dev-server URL when both exist; ports drift between runs (`bun dev` vs `bun preview`), file paths don't.
|
||||
|
||||
2. **Compute the slug.** Run:
|
||||
```bash
|
||||
node .trae-cn/skills/impeccable/scripts/critique-storage.mjs slug "<resolved-path-or-url>"
|
||||
```
|
||||
Keep the printed slug. It identifies this target's stream across runs. If the command exits non-zero ("no stable slug for input"), skip persistence for this run and tell the user; the trend won't update but the critique still goes ahead.
|
||||
|
||||
3. **Read the ignore list** at `.impeccable/critique/ignore.md` if it exists. Plain markdown; each non-empty, non-comment line is something the user has marked as "do not re-raise" (deferred tradeoffs, designer-intended deviations, detector false-positives the user accepts). When a finding's text matches a line here (case-insensitive substring against rule name or snippet), **drop it silently**. Do not mention it in the report. This is the ONLY input critique consumes from prior runs; anchoring on prior findings would defeat the point of independent assessment.
|
||||
|
||||
### Gather Assessments
|
||||
|
||||
Launch two independent assessments. **Neither may see the other's output.** This isolation is what makes the combined score honest. Running both in one head silently anchors them to each other; do not shortcut it for cost, speed, or context-size reasons.
|
||||
@@ -164,6 +182,36 @@ Provocative questions that might unlock better solutions:
|
||||
- Prioritize ruthlessly. If everything is important, nothing is.
|
||||
- Don't soften criticism. Developers need honest feedback to ship great design.
|
||||
|
||||
### Persist the Snapshot
|
||||
|
||||
Once the report above is finalized, write it to `.impeccable/critique/` so the user can refer back, and so `/impeccable polish` can pick up the priority issues without a copy-paste.
|
||||
|
||||
Skip this step if the Setup slug was null (vague or root-level target).
|
||||
|
||||
1. **Write the body to a temp file** so you can pipe it to the helper. Use the full report (heuristic table, anti-patterns verdict, priority issues, persona red flags) but stop before the "Ask the User" / "Recommended Actions" sections that come later.
|
||||
|
||||
2. **Pass the structured metadata** through `IMPECCABLE_CRITIQUE_META` (JSON), then run the write command:
|
||||
```bash
|
||||
IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"p0_count":<n>,"p1_count":<n>}' \
|
||||
node .trae-cn/skills/impeccable/scripts/critique-storage.mjs write <slug> <body-file>
|
||||
```
|
||||
The helper prints the absolute path it wrote.
|
||||
|
||||
3. **Read the trend** for context:
|
||||
```bash
|
||||
node .trae-cn/skills/impeccable/scripts/critique-storage.mjs trend <slug> 5
|
||||
```
|
||||
This returns a JSON array of the last 5 frontmatter entries (including the one you just wrote).
|
||||
|
||||
4. **Append a single line to the user-visible output**, after the report and before the questions:
|
||||
|
||||
> **Trend for `<slug>` (last 5 runs): 24 → 28 → 32 → 29 → 32**
|
||||
> Wrote `.impeccable/critique/<filename>`.
|
||||
|
||||
If this is the first run for the slug, the trend is just one score; say so: "First run for this target, no trend yet."
|
||||
|
||||
This is fire-and-forget. Do not show the user the helper's JSON output; only the human-readable trend line and the written path. Failures here should not block the rest of the flow; print the error and move on.
|
||||
|
||||
### Ask the User
|
||||
|
||||
**After presenting findings**, use targeted questions based on what was actually found. ask the user directly to clarify what you cannot infer. These answers will shape the action plan.
|
||||
|
||||
@@ -35,7 +35,14 @@ Understand the current state and goals before touching anything:
|
||||
- Loading and transition smoothness
|
||||
- Information architecture and flow drift (does this feature reveal complexity the way neighboring features do?)
|
||||
|
||||
4. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent; never perfect one corner while leaving another rough.
|
||||
4. **Pull in any prior critique** (optional signal): If `/impeccable critique` has been run on the same target, its priority issues are a useful prior for what to address first. Resolve the target to a file path or URL, then:
|
||||
```bash
|
||||
slug=$(node .trae-cn/skills/impeccable/scripts/critique-storage.mjs slug "<resolved>")
|
||||
node .trae-cn/skills/impeccable/scripts/critique-storage.mjs latest "$slug"
|
||||
```
|
||||
Exit 0 with body = found; fold the P0/P1 items into your polish list and mention the snapshot path so the user sees what you read. Exit 2 = no snapshot, continue without it. The critique is one input among many. Do your own pass either way.
|
||||
|
||||
5. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent; never perfect one corner while leaving another rough.
|
||||
|
||||
**CRITICAL**: Polish is the last step, not the first. Don't polish work that's not functionally complete.
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ After the discovery interview, generate a small set of visual direction probes *
|
||||
- The requested fidelity is **mid-fi, high-fi, or production-ready**. Skip for sketch-only planning.
|
||||
- The current harness gives you native image generation (Codex's `image_gen`, an equivalent MCP tool, or similar). Don't ask the user to install APIs or tooling.
|
||||
|
||||
When those conditions are met, this step is mandatory. If image generation isn't natively available, skip silently and proceed; don't announce the skip.
|
||||
When those conditions are met, this step is mandatory. If image generation isn't natively available, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed. The one-line announcement is required, not optional; it forces a conscious decision instead of letting the step quietly evaporate.
|
||||
|
||||
Use probes to explore visual lanes, not to replace the brief.
|
||||
|
||||
@@ -107,7 +107,7 @@ The probes should differ in primary visual direction (hierarchy, topology, densi
|
||||
- Do **not** treat generated imagery as final UX specification, final copy, or final accessibility behavior.
|
||||
- Do **not** use this step for minor refinements of existing work. It's for shaping a new surface or clarifying a big directional choice.
|
||||
|
||||
If image generation isn't natively available, skip this phase silently and proceed.
|
||||
If image generation isn't natively available, announce the skip in one line and proceed to the design brief.
|
||||
|
||||
## Phase 2: Design Brief
|
||||
|
||||
@@ -120,7 +120,7 @@ After the interview and any required probes, present a brief and **end your resp
|
||||
|
||||
Don't pad a clear brief into a long one to look thorough. A 70-line brief restating answers the user just gave is noise, not rigor. Equally, don't skip the confirmation pause to look efficient: the pause is the point.
|
||||
|
||||
If the user already said "approved" or "go" during discovery for the *exact direction you'd present*, that counts as confirmation; you may proceed without asking again. But if your brief adds anything the user hasn't seen and approved, you must stop and confirm.
|
||||
Present the brief, then **stop and wait for explicit confirmation**. You are not the judge of whether the user already approved. Even when the brief feels obviously right, ask once and wait. The pause is what separates shape from premature implementation.
|
||||
|
||||
### Brief Structure
|
||||
|
||||
@@ -158,8 +158,8 @@ Anything genuinely unresolved. Don't list "open questions" you've already recomm
|
||||
|
||||
---
|
||||
|
||||
ask the user directly to clarify what you cannot infer.
|
||||
ask the user directly to clarify what you cannot infer. Ask for explicit confirmation of the brief before finishing.
|
||||
|
||||
If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the user confirms direction (or the user already gave a clear go during discovery, which counts).
|
||||
If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the user confirms direction.
|
||||
|
||||
Once confirmed, the brief is complete. The user can now hand it to /impeccable, or use it to guide any other implementation approach. (If the user wants the full discovery-then-build flow in one step, they should use /impeccable craft instead, which runs this command internally.)
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Critique persistence helper.
|
||||
*
|
||||
* Each run of /impeccable critique writes a per-target snapshot to
|
||||
* .impeccable/critique/<timestamp>__<slug>.md
|
||||
* with a small YAML frontmatter carrying the score + P0/P1 counts.
|
||||
*
|
||||
* /impeccable polish reads the latest matching snapshot at start as its
|
||||
* fix backlog. No other skill auto-reads critique output.
|
||||
*
|
||||
* The slug is derived mechanically from the *resolved* primary artifact
|
||||
* (file path or URL), never from the user's natural-language phrasing.
|
||||
* Slug stability across runs is what lets the trend display work.
|
||||
*
|
||||
* CLI entry points (called from skill instructions):
|
||||
* node critique-storage.mjs slug <resolved-target>
|
||||
* node critique-storage.mjs write <slug> <snapshot-body-file>
|
||||
* node critique-storage.mjs latest <slug>
|
||||
* node critique-storage.mjs trend <slug> [limit]
|
||||
*
|
||||
* Note: there is intentionally no `ignore` subcommand. ignore.md is a plain
|
||||
* markdown file; the model reads it directly with its file-read tool. This
|
||||
* helper only exists for operations the model can't trivially do inline
|
||||
* (normalizing paths, generating filenames, globbing + parsing frontmatter).
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { getCritiqueDir } from './impeccable-paths.mjs';
|
||||
|
||||
const SLUG_MAX = 50;
|
||||
|
||||
/**
|
||||
* Mechanically derive a slug from a resolved target. Returns null if the
|
||||
* input doesn't look like a stable identifier (empty, project root, etc).
|
||||
*
|
||||
* Accepts file paths and URLs. The model resolves "the homepage" to a
|
||||
* concrete artifact before calling this — we never slug a natural-language
|
||||
* phrase.
|
||||
*/
|
||||
export function slugFromTarget(resolved, { cwd = process.cwd() } = {}) {
|
||||
if (!resolved || typeof resolved !== 'string') return null;
|
||||
const trimmed = resolved.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
// URL
|
||||
if (/^https?:\/\//i.test(trimmed)) {
|
||||
let url;
|
||||
try { url = new URL(trimmed); } catch { return null; }
|
||||
const hostPath = `${url.hostname}${url.pathname}`;
|
||||
return kebab(hostPath);
|
||||
}
|
||||
|
||||
// File path. Make it project-relative so two devs critiquing the same
|
||||
// checkout get the same slug regardless of where their repo is cloned.
|
||||
const abs = path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
|
||||
let rel = path.relative(cwd, abs);
|
||||
// If the target is outside cwd, fall back to the basename so we still
|
||||
// produce a stable slug (vs the absolute path, which would include
|
||||
// home dirs / usernames).
|
||||
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
||||
rel = path.basename(abs);
|
||||
}
|
||||
if (!rel || rel === '.' || rel === '') return null;
|
||||
return kebab(rel);
|
||||
}
|
||||
|
||||
function kebab(s) {
|
||||
const slug = s
|
||||
.toLowerCase()
|
||||
.replace(/[/\\.]+/g, '-')
|
||||
.replace(/[^a-z0-9-]+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
if (!slug) return null;
|
||||
// Cap from the tail — the tail (filename) is more identifying than the
|
||||
// top-level directory.
|
||||
return slug.length <= SLUG_MAX ? slug : slug.slice(slug.length - SLUG_MAX).replace(/^-/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filename-safe UTC ISO timestamp: hyphens for separators, trailing Z.
|
||||
* Plain colons aren't allowed on Windows filesystems.
|
||||
*/
|
||||
export function nowFilenameStamp(date = new Date()) {
|
||||
const iso = date.toISOString(); // 2026-05-12T18:30:00.123Z
|
||||
return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z');
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a snapshot for `slug`. `meta` carries the small structured frontmatter
|
||||
* keys read back by readTrend(). `body` is the human-readable critique
|
||||
* report (everything below the frontmatter).
|
||||
*
|
||||
* Returns the absolute path written.
|
||||
*/
|
||||
export function writeSnapshot({ slug, meta, body, cwd = process.cwd(), now = new Date() }) {
|
||||
if (!slug) throw new Error('writeSnapshot requires a slug');
|
||||
const dir = getCritiqueDir(cwd);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const timestamp = nowFilenameStamp(now);
|
||||
const filePath = path.join(dir, `${timestamp}__${slug}.md`);
|
||||
// Spread `meta` first so internally computed `timestamp` and `slug`
|
||||
// always win. Otherwise a caller-supplied meta blob (parsed from the
|
||||
// IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the
|
||||
// filename in disagreement with its frontmatter and corrupting trends.
|
||||
const front = serializeFrontmatter({ ...meta, timestamp, slug });
|
||||
fs.writeFileSync(filePath, `${front}\n${body.trim()}\n`, 'utf-8');
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function serializeFrontmatter(obj) {
|
||||
const lines = ['---'];
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
const str = typeof value === 'string' ? value : String(value);
|
||||
// Quote strings that contain : or # to keep parsing simple.
|
||||
const needsQuotes = typeof value === 'string' && /[:#]/.test(str);
|
||||
lines.push(`${key}: ${needsQuotes ? JSON.stringify(str) : str}`);
|
||||
}
|
||||
lines.push('---');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function parseFrontmatter(text) {
|
||||
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
if (!match) return {};
|
||||
const out = {};
|
||||
for (const line of match[1].split(/\r?\n/)) {
|
||||
const colon = line.indexOf(':');
|
||||
if (colon < 0) continue;
|
||||
const key = line.slice(0, colon).trim();
|
||||
let value = line.slice(colon + 1).trim();
|
||||
if (/^".*"$/.test(value)) {
|
||||
try { value = JSON.parse(value); } catch { /* leave as-is */ }
|
||||
} else if (/^-?\d+$/.test(value)) {
|
||||
value = Number(value);
|
||||
}
|
||||
out[key] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all snapshot files for `slug`, sorted oldest → newest.
|
||||
*/
|
||||
function listSnapshotsForSlug(slug, cwd) {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const suffix = `__${slug}.md`;
|
||||
return fs.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(suffix))
|
||||
.sort()
|
||||
.map((f) => path.join(dir, f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent snapshot for `slug`, or null. Polish reads this
|
||||
* to find its fix backlog when the slug matches.
|
||||
*/
|
||||
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
if (!all.length) return null;
|
||||
const latest = all[all.length - 1];
|
||||
const body = fs.readFileSync(latest, 'utf-8');
|
||||
return { path: latest, body, meta: parseFrontmatter(body) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last `limit` snapshots' frontmatter, oldest → newest.
|
||||
* Critique appends a one-line trend to its output using this.
|
||||
*/
|
||||
export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
const slice = all.slice(-limit);
|
||||
return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8')));
|
||||
}
|
||||
|
||||
// ---- CLI ---------------------------------------------------------------
|
||||
|
||||
function main(argv) {
|
||||
const [cmd, ...args] = argv;
|
||||
switch (cmd) {
|
||||
case 'slug': {
|
||||
const slug = slugFromTarget(args[0]);
|
||||
if (!slug) { process.stderr.write('no stable slug for input\n'); process.exit(1); }
|
||||
process.stdout.write(`${slug}\n`);
|
||||
return;
|
||||
}
|
||||
case 'write': {
|
||||
const [slug, bodyFile] = args;
|
||||
if (!slug || !bodyFile) { process.stderr.write('usage: write <slug> <body-file>\n'); process.exit(1); }
|
||||
const raw = fs.readFileSync(bodyFile, 'utf-8');
|
||||
// The body file may be a full report. The caller passes the meta as
|
||||
// a JSON object on stdin if it wants structured frontmatter; otherwise
|
||||
// we write with minimal metadata.
|
||||
let meta = {};
|
||||
const metaArg = process.env.IMPECCABLE_CRITIQUE_META;
|
||||
if (metaArg) {
|
||||
try { meta = JSON.parse(metaArg); } catch { /* ignore */ }
|
||||
}
|
||||
const out = writeSnapshot({ slug, meta, body: raw });
|
||||
process.stdout.write(`${out}\n`);
|
||||
return;
|
||||
}
|
||||
case 'latest': {
|
||||
const latest = readLatestSnapshot(args[0]);
|
||||
if (!latest) { process.exit(2); }
|
||||
process.stdout.write(latest.body);
|
||||
return;
|
||||
}
|
||||
case 'trend': {
|
||||
const rows = readTrend(args[0], { limit: args[1] ? Number(args[1]) : 5 });
|
||||
process.stdout.write(JSON.stringify(rows, null, 2) + '\n');
|
||||
return;
|
||||
}
|
||||
default:
|
||||
process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend> [args]\n');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main(process.argv.slice(2));
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import path from 'node:path';
|
||||
|
||||
export const IMPECCABLE_DIR = '.impeccable';
|
||||
export const LIVE_DIR = 'live';
|
||||
export const CRITIQUE_DIR = 'critique';
|
||||
|
||||
export function getImpeccableDir(cwd = process.cwd()) {
|
||||
return path.join(cwd, IMPECCABLE_DIR);
|
||||
@@ -96,6 +97,10 @@ export function getLiveAnnotationsDir(cwd = process.cwd()) {
|
||||
return path.join(getLiveDir(cwd), 'annotations');
|
||||
}
|
||||
|
||||
export function getCritiqueDir(cwd = process.cwd()) {
|
||||
return path.join(getImpeccableDir(cwd), CRITIQUE_DIR);
|
||||
}
|
||||
|
||||
export function getLegacyLiveAnnotationsDir(cwd = process.cwd()) {
|
||||
return path.join(cwd, '.impeccable-live', 'annotations');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: impeccable
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
version: 3.0.7
|
||||
version: 3.1.0
|
||||
user-invocable: true
|
||||
argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · teach|document|extract|live] [target]"
|
||||
license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution.
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# Codex: Visual Direction & Asset Production
|
||||
|
||||
This file is loaded by `/impeccable craft` when the harness has native image generation (currently Codex via `image_gen`). Other harnesses skip it. It covers the two craft steps that depend on real image generation: landing the visual direction, and producing the raster assets the implementation will compose.
|
||||
|
||||
Read this *before* generating any images. The order matters, and the per-step user pauses are what keep generated imagery from drifting away from the brief.
|
||||
|
||||
## Step A: Explore Directions with the User
|
||||
|
||||
Before generating anything, run a brief direction conversation grounded in the shape brief.
|
||||
|
||||
Ask **2-3 targeted questions** about visual lane, color strategy, atmosphere, and named anchor references. Don't enumerate generic menus; tie each question to the shape brief's answers. Example shape-grounded questions:
|
||||
|
||||
- "Brief says 'editorial restraint, Klim-adjacent.' Are we closer to a quiet specimen page or a magazine-spread feel with hero imagery?"
|
||||
- "Palette strategy from shape was 'Committed.' Want it warm-grounded (deep oxblood + cream) or cool-grounded (slate + paper white)?"
|
||||
|
||||
Stop and wait for answers. These pin the palette before any pixel gets generated.
|
||||
|
||||
## Step B: Generate the Brand Palette First
|
||||
|
||||
Generate **one** palette artifact before any mocks. This is a small, focused image: typography pairing on the chosen background, primary + accent color swatches, one signature ornament or motif. Single image, single pass.
|
||||
|
||||
Why palette first: mocks generated against a vague color sense produce noise that drowns out the structural decisions. A confirmed palette is the first concrete contract for everything downstream.
|
||||
|
||||
Show the palette to the user. Ask one question: "This is the palette I'm locking in for the mocks. Confirm, or call out what to shift?"
|
||||
|
||||
Wait for confirmation. Do not generate mocks against an unconfirmed palette.
|
||||
|
||||
## Step C: Generate 1-3 Visual Mocks Against the Palette
|
||||
|
||||
Once the palette is confirmed, generate **1 to 3** high-fidelity north-star comps. Each mock must use the confirmed palette and typography. Mocks differ in *structural* direction (hierarchy, topology, density, composition), not in color or motif.
|
||||
|
||||
- Brand work: push visual identity, composition, mood, and signature motifs.
|
||||
- Product work: push hierarchy, topology, density, tone, grounded in realistic product structure.
|
||||
- Landing pages and long-form brand surfaces: show enough of the second fold to establish the system beyond the hero.
|
||||
|
||||
Use the `image_gen` tool directly (or via the imagegen skill when available). Don't ask the user to install anything.
|
||||
|
||||
## Step D: Approval Loop
|
||||
|
||||
Show the comps. Ask what carries forward. Iterate until **one direction is approved** or the user explicitly delegates.
|
||||
|
||||
If the user delegates, pick the strongest direction and explain it from the brief, not personal taste.
|
||||
|
||||
Before moving to assets, summarize what to carry into code and what *not* to literalize from the mock. This is the handoff between visual exploration and semantic implementation.
|
||||
|
||||
## Step E: Mock Fidelity Inventory
|
||||
|
||||
Inventory the approved mock's major visible ingredients. For each, decide implementation: semantic HTML/CSS/SVG, generated raster, sourced raster, icon library, canvas/WebGL, or accepted omission.
|
||||
|
||||
Common ingredients to inventory:
|
||||
|
||||
- Hero silhouette and dominant composition
|
||||
- Signature motifs (planets, devices, portraits, charts, route lines, insets, badges, etc.)
|
||||
- Nav and primary CTA treatment
|
||||
- Section sequence, especially the second fold
|
||||
- Image-native content the concept depends on
|
||||
- Typography, density, color/material treatment, motion cues
|
||||
|
||||
Treat the mock as a north star, not a screenshot to trace. Don't rasterize core UI text. But if the live result lacks the mock's major ingredients, the implementation is wrong.
|
||||
|
||||
If a photographic, architectural, product, or place-led mock becomes generic CSS scenery, decorative diagrams, bullets, or copy, stop and fix it. That's a broken implementation, not a harmless interpretation.
|
||||
|
||||
Don't substitute a different hero composition or visual driver post-approval without user sign-off.
|
||||
|
||||
## Step F: Asset Slicing via the Asset Producer
|
||||
|
||||
Raster ingredients identified in Step E need clean production assets. Use the bundled `impeccable_asset_producer` subagent rather than producing inline.
|
||||
|
||||
Spawn it as a scoped subagent. If you do not have explicit permission to use agents, stop and ask:
|
||||
|
||||
```text
|
||||
Asset production will work better as a scoped subagent job. Should I spawn the Impeccable asset producer subagent for this step?
|
||||
```
|
||||
|
||||
Pass to the agent:
|
||||
|
||||
- Approved mock path or screenshot reference
|
||||
- Crop paths or a contact sheet with crop ids
|
||||
- Output directory
|
||||
- Required dimensions, format, transparency needs
|
||||
- Avoid list
|
||||
- Notes on what should remain semantic HTML/CSS/SVG instead of raster
|
||||
|
||||
Attach image generation capability to the spawned agent when the harness supports it. Do **not** load image-generation reference material into the parent thread.
|
||||
|
||||
Inline asset production is allowed only if the user declines subagents, the harness cannot spawn the authorized agent, or the user explicitly asks for single-thread mode.
|
||||
|
||||
Prefer HTML/CSS/SVG/canvas when they can credibly reproduce an ingredient; reach for real, generated, or stock imagery when the mock or subject matter calls for actual visual content.
|
||||
|
||||
## After This File
|
||||
|
||||
Once Steps A through F are complete, return to `craft.md` Step 5 (Build to Production Quality). The implementation builds against the confirmed palette, approved mock, and the assets the producer wrote.
|
||||
@@ -51,64 +51,15 @@ Then add references based on the brief's needs:
|
||||
- Responsive requirements? Consult [responsive-design.md](responsive-design.md)
|
||||
- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.md)
|
||||
|
||||
## Step 3: Land the Visual Direction (Capability-Gated)
|
||||
## Step 3: Visual Direction & Assets (Harness-Gated)
|
||||
|
||||
Generate high-fidelity visual comps before implementation when all three are true:
|
||||
If the harness has **native image generation** (currently Codex via `image_gen`), this step is mandatory. **Stop and load [codex.md](codex.md)**. It covers palette generation, mock exploration, the approval loop, mock-fidelity inventory, and asset slicing via the `impeccable_asset_producer` subagent. Follow Steps A-F in that file, then return here for Step 4.
|
||||
|
||||
- The work is net-new or visually open-ended enough that composition exploration will improve the build.
|
||||
- The brief's scope is mid-fi, high-fi, or production-ready.
|
||||
- The harness gives you native image generation (Codex's `image_gen`, an equivalent MCP tool). Don't ask the user to set up external APIs.
|
||||
If the harness lacks native image generation, **state in one line that the visual-direction-by-generation step is being skipped because the harness lacks native image generation, then proceed**. The one-line announcement is required; it forces a conscious decision instead of letting the step quietly evaporate. The brief is your only visual reference. Implement directly from it, treating any named anchor references and the brief's "Design Direction" as the contract.
|
||||
|
||||
When met, this step is mandatory for both brand and product work. If image generation isn't available, skip silently. "The eventual UI is semantic/code-native/accessible" is not a reason to skip; those are implementation requirements, not exploration ones.
|
||||
Whether you generated mocks or not: don't replace required imagery with generic cards, bullets, emoji, fake metrics, decorative CSS panels, or filler copy. Image-led briefs (restaurants, hotels, magazines, photography, hobbyist communities, food, travel, fashion, product) need real or sourced imagery in the build, not CSS scenery.
|
||||
|
||||
### What to generate
|
||||
|
||||
Generate **1 to 3** high-fidelity north-star comps from the confirmed brief. If shape already produced direction probes, resolve the winning lane further, not an unrelated exploration. Comps must differ in primary visual direction, not just color.
|
||||
|
||||
- Brand work: push visual identity, composition, and mood aggressively.
|
||||
- Product work: push hierarchy, topology, density, tone, grounded in realistic product structure.
|
||||
- Landing pages and long-form brand surfaces: show enough of the second fold to establish the system beyond the hero.
|
||||
|
||||
### Approval loop
|
||||
|
||||
Show the comps and ask what carries forward. Iterate until one direction is approved or the user delegates. If the user delegates, pick the strongest direction and explain it from the brief, not personal taste.
|
||||
|
||||
Before Step 4, summarize what to carry into code and what **not** to literalize from the mock. This is the handoff between visual exploration and semantic implementation.
|
||||
|
||||
### Mock fidelity inventory
|
||||
|
||||
Inventory the approved mock's major visible ingredients:
|
||||
|
||||
- Hero silhouette and dominant composition
|
||||
- Signature motifs (planets, devices, portraits, charts, route lines, insets, badges, etc.)
|
||||
- Nav and primary CTA treatment
|
||||
- Section sequence, especially the second fold
|
||||
- Image-native content the concept depends on
|
||||
- Typography, density, color/material treatment, motion cues
|
||||
|
||||
For each, decide implementation: semantic HTML/CSS/SVG, generated asset, sourced asset, icon library, canvas/WebGL, or accepted omission. Don't substitute a different hero composition or visual driver post-approval without user sign-off.
|
||||
|
||||
If a photographic, architectural, product, or place-led mock becomes generic CSS scenery / decorative diagrams / bullets / copy, stop and fix it. That's a broken implementation, not a harmless interpretation.
|
||||
|
||||
Treat the mock as a north star, not a screenshot to trace. Don't rasterize core UI text. But if the live result lacks the mock's major ingredients, the implementation is wrong.
|
||||
|
||||
## Step 4: Asset Extraction (Need-Gated)
|
||||
|
||||
If the approved direction needs raster assets, create them before building. Do not replace required imagery with generic cards, bullets, emoji, fake metrics, decorative CSS panels, or filler copy.
|
||||
|
||||
Use the native asset producer (`impeccable_asset_producer` in Codex, `impeccable-asset-producer` in Claude Code) to create clean assets from the hi-fi mock and crops. If you do not have explicit permission to use agents, stop and ask:
|
||||
|
||||
```text
|
||||
Asset production will work better as a scoped subagent job. Should I spawn the Impeccable asset producer subagent for this step?
|
||||
```
|
||||
|
||||
Do not skip asset production or silently do it inline. Inline asset production is allowed only if the user declines subagents, the harness cannot spawn the authorized agent, or the user explicitly asks for single-thread mode.
|
||||
|
||||
Pass the approved mock, crop/contact-sheet paths, output directory, dimensions/formats, transparency needs, constraints, and avoid list to the asset producer. Attach image generation capability to the spawned agent when the harness supports it; do not load image-generation reference material into the parent thread first.
|
||||
|
||||
Prefer HTML/CSS/SVG/canvas when they can credibly reproduce an ingredient; use real/generated/stock imagery when the mock or subject matter calls for actual visual content.
|
||||
|
||||
## Step 5: Build to Production Quality
|
||||
## Step 4: Build to Production Quality
|
||||
|
||||
Implement the feature following the design brief. Build in passes so structure, visual system, states, motion/media, and responsive behavior each get deliberate attention. The list below is the definition of done, not inspiration.
|
||||
|
||||
@@ -130,7 +81,7 @@ Implement the feature following the design brief. Build in passes so structure,
|
||||
- **Technically clean.** Production build passes, no console errors, no avoidable layout shift, no needless dependencies, no broken asset paths.
|
||||
- **Ask when uncertain.** If a discovery materially changes the brief or approved direction, stop and ask. Don't guess.
|
||||
|
||||
## Step 6: Iterate Visually
|
||||
## Step 5: Iterate Visually
|
||||
|
||||
Look at what you built like a designer would. Your eyes are whatever the harness gives you: a connected browser, a screenshotting tool, Playwright, or asking the user. Use them for responsive testing (mobile, tablet, desktop minimum) and general visual validation.
|
||||
|
||||
@@ -144,7 +95,7 @@ Actively check: responsive behavior (composes, not shrinks), every state (empty
|
||||
|
||||
Detector or QA output is defect evidence only; never proof the work is finished.
|
||||
|
||||
## Step 7: Present
|
||||
## Step 6: Present
|
||||
|
||||
Present the result to the user:
|
||||
- Show the feature in its primary state
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
> **Additional context needed**: what the interface is trying to accomplish.
|
||||
|
||||
### Setup: Resolve Target and Load Ignore List
|
||||
|
||||
Before gathering assessments, do two small bookkeeping steps. They cost almost nothing and they're what makes critique iterative across runs.
|
||||
|
||||
1. **Resolve the primary artifact.** The user's phrasing ("the homepage", "the pricing flow") is not stable enough to track across runs. Resolve it to a concrete file path or URL: the same one you'd already need to scan code or open in a browser. Examples:
|
||||
- "the homepage" → `site/pages/index.astro` (or `http://localhost:3000/` if you're inspecting live)
|
||||
- "the settings modal" → the primary component file (e.g., `src/components/Settings.tsx`)
|
||||
- "this page" → the URL or the page's source file
|
||||
Prefer the source file path over the dev-server URL when both exist; ports drift between runs (`bun dev` vs `bun preview`), file paths don't.
|
||||
|
||||
2. **Compute the slug.** Run:
|
||||
```bash
|
||||
node .trae/skills/impeccable/scripts/critique-storage.mjs slug "<resolved-path-or-url>"
|
||||
```
|
||||
Keep the printed slug. It identifies this target's stream across runs. If the command exits non-zero ("no stable slug for input"), skip persistence for this run and tell the user; the trend won't update but the critique still goes ahead.
|
||||
|
||||
3. **Read the ignore list** at `.impeccable/critique/ignore.md` if it exists. Plain markdown; each non-empty, non-comment line is something the user has marked as "do not re-raise" (deferred tradeoffs, designer-intended deviations, detector false-positives the user accepts). When a finding's text matches a line here (case-insensitive substring against rule name or snippet), **drop it silently**. Do not mention it in the report. This is the ONLY input critique consumes from prior runs; anchoring on prior findings would defeat the point of independent assessment.
|
||||
|
||||
### Gather Assessments
|
||||
|
||||
Launch two independent assessments. **Neither may see the other's output.** This isolation is what makes the combined score honest. Running both in one head silently anchors them to each other; do not shortcut it for cost, speed, or context-size reasons.
|
||||
@@ -164,6 +182,36 @@ Provocative questions that might unlock better solutions:
|
||||
- Prioritize ruthlessly. If everything is important, nothing is.
|
||||
- Don't soften criticism. Developers need honest feedback to ship great design.
|
||||
|
||||
### Persist the Snapshot
|
||||
|
||||
Once the report above is finalized, write it to `.impeccable/critique/` so the user can refer back, and so `/impeccable polish` can pick up the priority issues without a copy-paste.
|
||||
|
||||
Skip this step if the Setup slug was null (vague or root-level target).
|
||||
|
||||
1. **Write the body to a temp file** so you can pipe it to the helper. Use the full report (heuristic table, anti-patterns verdict, priority issues, persona red flags) but stop before the "Ask the User" / "Recommended Actions" sections that come later.
|
||||
|
||||
2. **Pass the structured metadata** through `IMPECCABLE_CRITIQUE_META` (JSON), then run the write command:
|
||||
```bash
|
||||
IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"p0_count":<n>,"p1_count":<n>}' \
|
||||
node .trae/skills/impeccable/scripts/critique-storage.mjs write <slug> <body-file>
|
||||
```
|
||||
The helper prints the absolute path it wrote.
|
||||
|
||||
3. **Read the trend** for context:
|
||||
```bash
|
||||
node .trae/skills/impeccable/scripts/critique-storage.mjs trend <slug> 5
|
||||
```
|
||||
This returns a JSON array of the last 5 frontmatter entries (including the one you just wrote).
|
||||
|
||||
4. **Append a single line to the user-visible output**, after the report and before the questions:
|
||||
|
||||
> **Trend for `<slug>` (last 5 runs): 24 → 28 → 32 → 29 → 32**
|
||||
> Wrote `.impeccable/critique/<filename>`.
|
||||
|
||||
If this is the first run for the slug, the trend is just one score; say so: "First run for this target, no trend yet."
|
||||
|
||||
This is fire-and-forget. Do not show the user the helper's JSON output; only the human-readable trend line and the written path. Failures here should not block the rest of the flow; print the error and move on.
|
||||
|
||||
### Ask the User
|
||||
|
||||
**After presenting findings**, use targeted questions based on what was actually found. ask the user directly to clarify what you cannot infer. These answers will shape the action plan.
|
||||
|
||||
@@ -35,7 +35,14 @@ Understand the current state and goals before touching anything:
|
||||
- Loading and transition smoothness
|
||||
- Information architecture and flow drift (does this feature reveal complexity the way neighboring features do?)
|
||||
|
||||
4. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent; never perfect one corner while leaving another rough.
|
||||
4. **Pull in any prior critique** (optional signal): If `/impeccable critique` has been run on the same target, its priority issues are a useful prior for what to address first. Resolve the target to a file path or URL, then:
|
||||
```bash
|
||||
slug=$(node .trae/skills/impeccable/scripts/critique-storage.mjs slug "<resolved>")
|
||||
node .trae/skills/impeccable/scripts/critique-storage.mjs latest "$slug"
|
||||
```
|
||||
Exit 0 with body = found; fold the P0/P1 items into your polish list and mention the snapshot path so the user sees what you read. Exit 2 = no snapshot, continue without it. The critique is one input among many. Do your own pass either way.
|
||||
|
||||
5. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent; never perfect one corner while leaving another rough.
|
||||
|
||||
**CRITICAL**: Polish is the last step, not the first. Don't polish work that's not functionally complete.
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ After the discovery interview, generate a small set of visual direction probes *
|
||||
- The requested fidelity is **mid-fi, high-fi, or production-ready**. Skip for sketch-only planning.
|
||||
- The current harness gives you native image generation (Codex's `image_gen`, an equivalent MCP tool, or similar). Don't ask the user to install APIs or tooling.
|
||||
|
||||
When those conditions are met, this step is mandatory. If image generation isn't natively available, skip silently and proceed; don't announce the skip.
|
||||
When those conditions are met, this step is mandatory. If image generation isn't natively available, do not ask the user to install APIs or tooling. State in one line that the image step is skipped because the harness lacks native image generation, then proceed. The one-line announcement is required, not optional; it forces a conscious decision instead of letting the step quietly evaporate.
|
||||
|
||||
Use probes to explore visual lanes, not to replace the brief.
|
||||
|
||||
@@ -107,7 +107,7 @@ The probes should differ in primary visual direction (hierarchy, topology, densi
|
||||
- Do **not** treat generated imagery as final UX specification, final copy, or final accessibility behavior.
|
||||
- Do **not** use this step for minor refinements of existing work. It's for shaping a new surface or clarifying a big directional choice.
|
||||
|
||||
If image generation isn't natively available, skip this phase silently and proceed.
|
||||
If image generation isn't natively available, announce the skip in one line and proceed to the design brief.
|
||||
|
||||
## Phase 2: Design Brief
|
||||
|
||||
@@ -120,7 +120,7 @@ After the interview and any required probes, present a brief and **end your resp
|
||||
|
||||
Don't pad a clear brief into a long one to look thorough. A 70-line brief restating answers the user just gave is noise, not rigor. Equally, don't skip the confirmation pause to look efficient: the pause is the point.
|
||||
|
||||
If the user already said "approved" or "go" during discovery for the *exact direction you'd present*, that counts as confirmation; you may proceed without asking again. But if your brief adds anything the user hasn't seen and approved, you must stop and confirm.
|
||||
Present the brief, then **stop and wait for explicit confirmation**. You are not the judge of whether the user already approved. Even when the brief feels obviously right, ask once and wait. The pause is what separates shape from premature implementation.
|
||||
|
||||
### Brief Structure
|
||||
|
||||
@@ -158,8 +158,8 @@ Anything genuinely unresolved. Don't list "open questions" you've already recomm
|
||||
|
||||
---
|
||||
|
||||
ask the user directly to clarify what you cannot infer.
|
||||
ask the user directly to clarify what you cannot infer. Ask for explicit confirmation of the brief before finishing.
|
||||
|
||||
If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the user confirms direction (or the user already gave a clear go during discovery, which counts).
|
||||
If the user disagrees with any part, revisit the relevant discovery questions. A shape run is incomplete until the user confirms direction.
|
||||
|
||||
Once confirmed, the brief is complete. The user can now hand it to /impeccable, or use it to guide any other implementation approach. (If the user wants the full discovery-then-build flow in one step, they should use /impeccable craft instead, which runs this command internally.)
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Critique persistence helper.
|
||||
*
|
||||
* Each run of /impeccable critique writes a per-target snapshot to
|
||||
* .impeccable/critique/<timestamp>__<slug>.md
|
||||
* with a small YAML frontmatter carrying the score + P0/P1 counts.
|
||||
*
|
||||
* /impeccable polish reads the latest matching snapshot at start as its
|
||||
* fix backlog. No other skill auto-reads critique output.
|
||||
*
|
||||
* The slug is derived mechanically from the *resolved* primary artifact
|
||||
* (file path or URL), never from the user's natural-language phrasing.
|
||||
* Slug stability across runs is what lets the trend display work.
|
||||
*
|
||||
* CLI entry points (called from skill instructions):
|
||||
* node critique-storage.mjs slug <resolved-target>
|
||||
* node critique-storage.mjs write <slug> <snapshot-body-file>
|
||||
* node critique-storage.mjs latest <slug>
|
||||
* node critique-storage.mjs trend <slug> [limit]
|
||||
*
|
||||
* Note: there is intentionally no `ignore` subcommand. ignore.md is a plain
|
||||
* markdown file; the model reads it directly with its file-read tool. This
|
||||
* helper only exists for operations the model can't trivially do inline
|
||||
* (normalizing paths, generating filenames, globbing + parsing frontmatter).
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { getCritiqueDir } from './impeccable-paths.mjs';
|
||||
|
||||
const SLUG_MAX = 50;
|
||||
|
||||
/**
|
||||
* Mechanically derive a slug from a resolved target. Returns null if the
|
||||
* input doesn't look like a stable identifier (empty, project root, etc).
|
||||
*
|
||||
* Accepts file paths and URLs. The model resolves "the homepage" to a
|
||||
* concrete artifact before calling this — we never slug a natural-language
|
||||
* phrase.
|
||||
*/
|
||||
export function slugFromTarget(resolved, { cwd = process.cwd() } = {}) {
|
||||
if (!resolved || typeof resolved !== 'string') return null;
|
||||
const trimmed = resolved.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
// URL
|
||||
if (/^https?:\/\//i.test(trimmed)) {
|
||||
let url;
|
||||
try { url = new URL(trimmed); } catch { return null; }
|
||||
const hostPath = `${url.hostname}${url.pathname}`;
|
||||
return kebab(hostPath);
|
||||
}
|
||||
|
||||
// File path. Make it project-relative so two devs critiquing the same
|
||||
// checkout get the same slug regardless of where their repo is cloned.
|
||||
const abs = path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
|
||||
let rel = path.relative(cwd, abs);
|
||||
// If the target is outside cwd, fall back to the basename so we still
|
||||
// produce a stable slug (vs the absolute path, which would include
|
||||
// home dirs / usernames).
|
||||
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
||||
rel = path.basename(abs);
|
||||
}
|
||||
if (!rel || rel === '.' || rel === '') return null;
|
||||
return kebab(rel);
|
||||
}
|
||||
|
||||
function kebab(s) {
|
||||
const slug = s
|
||||
.toLowerCase()
|
||||
.replace(/[/\\.]+/g, '-')
|
||||
.replace(/[^a-z0-9-]+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
if (!slug) return null;
|
||||
// Cap from the tail — the tail (filename) is more identifying than the
|
||||
// top-level directory.
|
||||
return slug.length <= SLUG_MAX ? slug : slug.slice(slug.length - SLUG_MAX).replace(/^-/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filename-safe UTC ISO timestamp: hyphens for separators, trailing Z.
|
||||
* Plain colons aren't allowed on Windows filesystems.
|
||||
*/
|
||||
export function nowFilenameStamp(date = new Date()) {
|
||||
const iso = date.toISOString(); // 2026-05-12T18:30:00.123Z
|
||||
return iso.replace(/[:.]/g, '-').replace(/-\d+Z$/, 'Z');
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a snapshot for `slug`. `meta` carries the small structured frontmatter
|
||||
* keys read back by readTrend(). `body` is the human-readable critique
|
||||
* report (everything below the frontmatter).
|
||||
*
|
||||
* Returns the absolute path written.
|
||||
*/
|
||||
export function writeSnapshot({ slug, meta, body, cwd = process.cwd(), now = new Date() }) {
|
||||
if (!slug) throw new Error('writeSnapshot requires a slug');
|
||||
const dir = getCritiqueDir(cwd);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const timestamp = nowFilenameStamp(now);
|
||||
const filePath = path.join(dir, `${timestamp}__${slug}.md`);
|
||||
// Spread `meta` first so internally computed `timestamp` and `slug`
|
||||
// always win. Otherwise a caller-supplied meta blob (parsed from the
|
||||
// IMPECCABLE_CRITIQUE_META env var) could clobber them, leaving the
|
||||
// filename in disagreement with its frontmatter and corrupting trends.
|
||||
const front = serializeFrontmatter({ ...meta, timestamp, slug });
|
||||
fs.writeFileSync(filePath, `${front}\n${body.trim()}\n`, 'utf-8');
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function serializeFrontmatter(obj) {
|
||||
const lines = ['---'];
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
const str = typeof value === 'string' ? value : String(value);
|
||||
// Quote strings that contain : or # to keep parsing simple.
|
||||
const needsQuotes = typeof value === 'string' && /[:#]/.test(str);
|
||||
lines.push(`${key}: ${needsQuotes ? JSON.stringify(str) : str}`);
|
||||
}
|
||||
lines.push('---');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function parseFrontmatter(text) {
|
||||
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
if (!match) return {};
|
||||
const out = {};
|
||||
for (const line of match[1].split(/\r?\n/)) {
|
||||
const colon = line.indexOf(':');
|
||||
if (colon < 0) continue;
|
||||
const key = line.slice(0, colon).trim();
|
||||
let value = line.slice(colon + 1).trim();
|
||||
if (/^".*"$/.test(value)) {
|
||||
try { value = JSON.parse(value); } catch { /* leave as-is */ }
|
||||
} else if (/^-?\d+$/.test(value)) {
|
||||
value = Number(value);
|
||||
}
|
||||
out[key] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all snapshot files for `slug`, sorted oldest → newest.
|
||||
*/
|
||||
function listSnapshotsForSlug(slug, cwd) {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const suffix = `__${slug}.md`;
|
||||
return fs.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(suffix))
|
||||
.sort()
|
||||
.map((f) => path.join(dir, f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent snapshot for `slug`, or null. Polish reads this
|
||||
* to find its fix backlog when the slug matches.
|
||||
*/
|
||||
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
if (!all.length) return null;
|
||||
const latest = all[all.length - 1];
|
||||
const body = fs.readFileSync(latest, 'utf-8');
|
||||
return { path: latest, body, meta: parseFrontmatter(body) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last `limit` snapshots' frontmatter, oldest → newest.
|
||||
* Critique appends a one-line trend to its output using this.
|
||||
*/
|
||||
export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
const slice = all.slice(-limit);
|
||||
return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8')));
|
||||
}
|
||||
|
||||
// ---- CLI ---------------------------------------------------------------
|
||||
|
||||
function main(argv) {
|
||||
const [cmd, ...args] = argv;
|
||||
switch (cmd) {
|
||||
case 'slug': {
|
||||
const slug = slugFromTarget(args[0]);
|
||||
if (!slug) { process.stderr.write('no stable slug for input\n'); process.exit(1); }
|
||||
process.stdout.write(`${slug}\n`);
|
||||
return;
|
||||
}
|
||||
case 'write': {
|
||||
const [slug, bodyFile] = args;
|
||||
if (!slug || !bodyFile) { process.stderr.write('usage: write <slug> <body-file>\n'); process.exit(1); }
|
||||
const raw = fs.readFileSync(bodyFile, 'utf-8');
|
||||
// The body file may be a full report. The caller passes the meta as
|
||||
// a JSON object on stdin if it wants structured frontmatter; otherwise
|
||||
// we write with minimal metadata.
|
||||
let meta = {};
|
||||
const metaArg = process.env.IMPECCABLE_CRITIQUE_META;
|
||||
if (metaArg) {
|
||||
try { meta = JSON.parse(metaArg); } catch { /* ignore */ }
|
||||
}
|
||||
const out = writeSnapshot({ slug, meta, body: raw });
|
||||
process.stdout.write(`${out}\n`);
|
||||
return;
|
||||
}
|
||||
case 'latest': {
|
||||
const latest = readLatestSnapshot(args[0]);
|
||||
if (!latest) { process.exit(2); }
|
||||
process.stdout.write(latest.body);
|
||||
return;
|
||||
}
|
||||
case 'trend': {
|
||||
const rows = readTrend(args[0], { limit: args[1] ? Number(args[1]) : 5 });
|
||||
process.stdout.write(JSON.stringify(rows, null, 2) + '\n');
|
||||
return;
|
||||
}
|
||||
default:
|
||||
process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend> [args]\n');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main(process.argv.slice(2));
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import path from 'node:path';
|
||||
|
||||
export const IMPECCABLE_DIR = '.impeccable';
|
||||
export const LIVE_DIR = 'live';
|
||||
export const CRITIQUE_DIR = 'critique';
|
||||
|
||||
export function getImpeccableDir(cwd = process.cwd()) {
|
||||
return path.join(cwd, IMPECCABLE_DIR);
|
||||
@@ -96,6 +97,10 @@ export function getLiveAnnotationsDir(cwd = process.cwd()) {
|
||||
return path.join(getLiveDir(cwd), 'annotations');
|
||||
}
|
||||
|
||||
export function getCritiqueDir(cwd = process.cwd()) {
|
||||
return path.join(getImpeccableDir(cwd), CRITIQUE_DIR);
|
||||
}
|
||||
|
||||
export function getLegacyLiveAnnotationsDir(cwd = process.cwd()) {
|
||||
return path.join(cwd, '.impeccable-live', 'annotations');
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "impeccable",
|
||||
"version": "2.1.8",
|
||||
"version": "2.1.9",
|
||||
"author": "Paul Bakaus",
|
||||
"description": "Design skills, commands, and anti-pattern detection for AI coding agents",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "impeccable",
|
||||
"description": "Design fluency for frontend development. 1 skill with 23 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
|
||||
"version": "3.0.7",
|
||||
"version": "3.1.0",
|
||||
"author": {
|
||||
"name": "Paul Bakaus",
|
||||
"email": "paul@paulbakaus.com"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user