qualia-framework 5.9.1 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/AGENTS.md +2 -1
  2. package/CLAUDE.md +2 -1
  3. package/README.md +14 -7
  4. package/agents/builder.md +1 -5
  5. package/agents/plan-checker.md +1 -1
  6. package/agents/planner.md +2 -6
  7. package/agents/qa-browser.md +3 -3
  8. package/agents/roadmapper.md +1 -1
  9. package/agents/verifier.md +7 -9
  10. package/agents/visual-evaluator.md +1 -3
  11. package/bin/cli.js +32 -6
  12. package/bin/slop-detect.mjs +81 -9
  13. package/docs/archive/CHANGELOG-pre-v4.md +855 -0
  14. package/docs/onboarding.html +2 -2
  15. package/guide.md +15 -2
  16. package/hooks/auto-update.js +6 -3
  17. package/hooks/env-empty-guard.js +5 -4
  18. package/hooks/pre-compact.js +5 -3
  19. package/hooks/pre-push.js +57 -0
  20. package/package.json +2 -2
  21. package/qualia-design/design-reference.md +2 -1
  22. package/qualia-design/frontend.md +4 -4
  23. package/rules/one-opinion.md +59 -0
  24. package/rules/trust-boundary.md +35 -0
  25. package/skills/qualia-feature/SKILL.md +5 -5
  26. package/skills/qualia-flush/SKILL.md +5 -7
  27. package/skills/qualia-hook-gen/SKILL.md +1 -1
  28. package/skills/qualia-learn/SKILL.md +1 -0
  29. package/skills/qualia-map/SKILL.md +1 -0
  30. package/skills/qualia-milestone/SKILL.md +1 -1
  31. package/skills/qualia-new/SKILL.md +6 -6
  32. package/skills/qualia-plan/SKILL.md +1 -1
  33. package/skills/qualia-polish/REFERENCE.md +8 -6
  34. package/skills/qualia-polish/SKILL.md +9 -7
  35. package/skills/qualia-polish/scripts/loop.mjs +18 -6
  36. package/skills/qualia-postmortem/SKILL.md +1 -1
  37. package/skills/qualia-report/SKILL.md +2 -1
  38. package/skills/qualia-road/SKILL.md +16 -4
  39. package/skills/qualia-verify/SKILL.md +2 -2
  40. package/skills/qualia-vibe/SKILL.md +226 -0
  41. package/skills/qualia-vibe/scripts/extract.mjs +141 -0
  42. package/skills/qualia-vibe/scripts/tokens.mjs +342 -0
  43. package/templates/help.html +9 -2
  44. package/tests/bin.test.sh +12 -12
  45. package/tests/refs.test.sh +1 -1
  46. package/tests/run-all.sh +48 -0
  47. package/tests/slop-detect.test.sh +11 -5
@@ -29,7 +29,7 @@
29
29
  */
30
30
 
31
31
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
32
- import { dirname } from "node:path";
32
+ import path, { dirname } from "node:path";
33
33
  import { argv, exit } from "node:process";
34
34
  import { spawnSync } from "node:child_process";
35
35
 
@@ -202,10 +202,17 @@ function cmdCommitFix() {
202
202
  if (!statePath || !file) { console.error("--state and --file required"); exit(2); }
203
203
  const state = loadState(statePath);
204
204
 
205
- // slop-detect gate — block on critical findings
205
+ // slop-detect gate — block on critical findings. Resolve the script path with
206
+ // a search order so installs that put slop-detect in non-default locations
207
+ // still gate. Silent skip is reserved for genuinely missing installs.
206
208
  const slopBin = process.env.SLOP_DETECT_BIN || "node";
207
- const slopScript = process.env.SLOP_DETECT_SCRIPT || `${process.env.HOME}/.claude/bin/slop-detect.mjs`;
208
- if (existsSync(slopScript)) {
209
+ const slopCandidates = [
210
+ process.env.SLOP_DETECT_SCRIPT,
211
+ `${process.env.HOME}/.claude/bin/slop-detect.mjs`,
212
+ path.join(path.dirname(new URL(import.meta.url).pathname), "..", "..", "..", "bin", "slop-detect.mjs"),
213
+ ].filter(Boolean);
214
+ const slopScript = slopCandidates.find((p) => existsSync(p));
215
+ if (slopScript) {
209
216
  const r = spawnSync(slopBin, [slopScript, file], { encoding: "utf8" });
210
217
  if (r.status === 1) {
211
218
  console.log(JSON.stringify({ ok: false, gate: "slop-detect", file, output: r.stdout }, null, 2));
@@ -213,12 +220,17 @@ function cmdCommitFix() {
213
220
  }
214
221
  }
215
222
 
216
- // Stage + commit
223
+ // Stage + commit. Set a bot identity inline so the commit works on fresh
224
+ // clones where git user.name / user.email aren't configured yet.
217
225
  const safeSlug = String(slug).toLowerCase().replace(/[^a-z0-9-]+/g, "-").slice(0, 48);
218
226
  const msg = `qpl-${state.iteration}: ${safeSlug}`;
219
227
  const add = spawnSync("git", ["add", file], { encoding: "utf8" });
220
228
  if (add.status !== 0) { console.error(add.stderr); exit(2); }
221
- const commit = spawnSync("git", ["commit", "-m", msg], { encoding: "utf8" });
229
+ const commit = spawnSync("git", [
230
+ "-c", "user.name=Qualia Polish Loop",
231
+ "-c", "user.email=polish-loop@qualia.solutions",
232
+ "commit", "-m", msg,
233
+ ], { encoding: "utf8" });
222
234
  if (commit.status !== 0) {
223
235
  // empty diff → nothing to commit; not fatal
224
236
  if (/nothing to commit/i.test(commit.stdout + commit.stderr)) {
@@ -95,7 +95,7 @@ matches the failure. Use this lookup:
95
95
  | Wave 2 task ran before wave 1 committed | `agents/planner.md` (dependency graph) |
96
96
  | Build passed locally, broke in CI | `rules/deployment.md` or a missing pre-deploy-gate scan |
97
97
  | RLS missing on new table | `rules/security.md` + `agents/builder.md` (security persona handling) |
98
- | Design regression — fonts off, contrast fail | `qualia-design/frontend.md` + `skills/qualia-design/SKILL.md` |
98
+ | Design regression — fonts off, contrast fail | `qualia-design/frontend.md` + `qualia-design/design-laws.md` + `skills/qualia-polish/SKILL.md` |
99
99
  | Migration unsafe (DROP without IF EXISTS, etc.) | `hooks/migration-guard.js` |
100
100
  | Verifier missed it | `agents/verifier.md` — most embarrassing case, address with extra care |
101
101
 
@@ -221,7 +221,8 @@ PAYLOAD=$(
221
221
  tasks_done:t.tasks_done||0,tasks_total:t.tasks_total||0,verification:t.verification||'pending',
222
222
  gap_cycles:(t.gap_cycles||{})[String(t.phase)]||0,build_count:t.build_count||0,
223
223
  deploy_count:t.deploy_count||0,deployed_url:t.deployed_url||'',
224
- session_started_at:t.session_started_at||'',last_pushed_at:t.last_pushed_at||'',
224
+ ...(t.session_started_at?{session_started_at:t.session_started_at}:{}),
225
+ ...(t.last_pushed_at?{last_pushed_at:t.last_pushed_at}:{}),
225
226
  session_duration_minutes:sessionDurationMinutes,
226
227
  lifetime:t.lifetime||{},commits:commits,notes:notes,
227
228
  submitted_by:process.env.SUBMITTED_BY||'unknown',submitted_at:process.env.SUBMITTED_AT
@@ -32,7 +32,7 @@ Final milestone = Handoff:
32
32
  Done.
33
33
  ```
34
34
 
35
- ## Design as a thread (v4.5.0+)
35
+ ## Design as a thread
36
36
  Every road agent loads `PRODUCT.md + DESIGN.md + design-laws.md` substrate. Builders run `slop-detect` on every frontend commit. Verifiers score 8 design dimensions per phase.
37
37
 
38
38
  ## /qualia-polish is scope-adaptive
@@ -45,7 +45,19 @@ Every road agent loads `PRODUCT.md + DESIGN.md + design-laws.md` substrate. Buil
45
45
  /qualia-polish --quick ~1m gates only
46
46
  ```
47
47
 
48
- ## /qualia-polish --loop -- autonomous visual QA (v5.1+, consolidated into /qualia-polish in v5.8)
48
+ ## Design pivots /qualia-vibe (v6.1+)
49
+ ```
50
+ /qualia-vibe fast aesthetic pivot: ONE proposed direction, swap tokens, keep layout (~3 min)
51
+ /qualia-vibe brutalist explicit pivot to named direction
52
+ /qualia-vibe --variants 3 opt-in menu (uses AskUserQuestion; default flow is one-opinion)
53
+ /qualia-vibe --extract https://stripe.com reverse-engineer DESIGN.md from a reference URL
54
+ /qualia-vibe --extract ./inspo.png same, from a local screenshot
55
+ /qualia-vibe --sync show drift between code (CSS vars, Tailwind config) and DESIGN.md
56
+ /qualia-vibe --sync --write patch DESIGN.md to match code, commit
57
+ ```
58
+ `/qualia-vibe` is for the WHOLE-SITE aesthetic. For surgical component-level fixes use `/qualia-polish` (component or section scope). For ground-up structural redesign use `/qualia-polish --redesign`.
59
+
60
+ ## /qualia-polish --loop — autonomous visual QA
49
61
  ```
50
62
  /qualia-polish --loop http://localhost:3000 screenshot + eval + fix loop
51
63
  /qualia-polish --loop {url} --max 4 cap iterations
@@ -55,14 +67,14 @@ Every road agent loads `PRODUCT.md + DESIGN.md + design-laws.md` substrate. Buil
55
67
  ```
56
68
  Screenshots at 3 viewports (375/768/1440), scores 8 design dimensions using vision, fixes issues, re-screenshots, loops until all dims >= 3 or kill-switch triggers. Per-iteration git commits for clean revert.
57
69
 
58
- ## v5.3+ skills
70
+ ## Deterministic-enforcement skills
59
71
  ```
60
72
  /qualia-hook-gen convert a CLAUDE.md/rules instruction into a deterministic pre-tool-use hook
61
73
  /qualia-optimize --deepen spawns 3 parallel interface-design variants per candidate (Step 5b)
62
74
  ```
63
75
  `/qualia-hook-gen` reduces lifetime token cost (each migrated rule frees ~50-200 tokens per request). `/qualia-optimize --deepen` produces dramatically better refactor RFCs because 3 radically-different interfaces are surfaced and the human picks/hybridizes.
64
76
 
65
- ## Alignment substrate (v5.0+)
77
+ ## Alignment substrate
66
78
  Before high-stakes phases, run alignment skills against `.planning/CONTEXT.md` (domain glossary) and `.planning/decisions/` (ADRs):
67
79
 
68
80
  ```
@@ -19,7 +19,7 @@ Spawn verifier to check phase goal. Does NOT trust build summaries; greps codeba
19
19
  `/qualia-verify` — verify current built phase
20
20
  `/qualia-verify {N}` — verify specific phase
21
21
  `/qualia-verify {N} --auto` — verify + auto-chain: PASS → next phase/milestone; FAIL → gap closure; gap limit → halt
22
- `/qualia-verify {N} --adversarial` — second verifier in fresh context with adversarial prompt. Union findings. Recommended for high-stakes phases (Handoff, payment/auth/migration). v4.3.0+.
22
+ `/qualia-verify {N} --adversarial` — second verifier in fresh context with adversarial prompt. Union findings. Recommended for high-stakes phases (Handoff, payment/auth/migration).
23
23
 
24
24
  ## Process
25
25
 
@@ -142,7 +142,7 @@ Per gap:
142
142
  node ~/.claude/bin/qualia-ui.js fail "{gap description}"
143
143
  ```
144
144
 
145
- **Self-healing (v4.3.0+):** before re-planning gaps, run postmortem so framework learns from miss. Writes `.planning/phase-{N}-postmortem.md`. Does NOT auto-apply deltas unless user runs `/qualia-postmortem --apply`.
145
+ **Self-healing:** before re-planning gaps, run postmortem so the framework learns from the miss. Writes `.planning/phase-{N}-postmortem.md`. Does NOT auto-apply deltas unless user runs `/qualia-postmortem --apply`.
146
146
 
147
147
  ```
148
148
  /qualia-postmortem --phase {N}
@@ -0,0 +1,226 @@
1
+ ---
2
+ name: qualia-vibe
3
+ description: "Fast aesthetic pivot — swap the design vibe (tokens: color, typography, motion, depth) without touching component structure or layout. Default mode proposes ONE direction (per rules/one-opinion.md). Sub-modes: --variants for A/B/C menu (only when user explicitly asked), --extract URL to reverse-engineer DESIGN.md from a reference site, --sync to back-sync code → DESIGN.md when tokens drifted in code. Triggers: 'different vibe', 'change the look', 'swap the aesthetic', 'try something bolder', 'redesign the look', 'match this site', 'sync the design file'."
4
+ disable-model-invocation: false
5
+ allowed-tools:
6
+ - Bash
7
+ - Read
8
+ - Write
9
+ - Edit
10
+ - Glob
11
+ - Grep
12
+ - AskUserQuestion
13
+ ---
14
+
15
+ # /qualia-vibe — Fast Aesthetic Pivot
16
+
17
+ Swap the **vibe** without redoing the **app**. Tokens only: color, typography, depth, motion, brand accents. Component structure, routing, data flow, and layout grid stay exactly where they were. This is the difference between `/qualia-vibe` (~3 min) and `/qualia-polish --redesign` (~30 min).
18
+
19
+ ## When to use
20
+
21
+ - Client said "I want a different vibe" / "make it look bolder" / "this feels too startup-y" → `/qualia-vibe`
22
+ - You want to test if a different aesthetic is right BEFORE committing the redesign → `/qualia-vibe`
23
+ - You want to match a reference site's design system → `/qualia-vibe --extract https://example.com`
24
+ - Someone changed CSS vars or Tailwind config directly and DESIGN.md is out of sync → `/qualia-vibe --sync`
25
+ - You want to enumerate options (rare — see `rules/one-opinion.md`) → `/qualia-vibe --variants 3`
26
+
27
+ ## When NOT to use
28
+
29
+ - The complaint is "this component is broken" or "the spacing is wrong" → `/qualia-polish` (component or section scope).
30
+ - The site has no DESIGN.md yet → run `/qualia-new` first; vibe is a PIVOT, not a cold start.
31
+ - You want a structural redesign (layout, navigation, information architecture) → `/qualia-polish --redesign` (~30 min ground-up).
32
+
33
+ ## Layout-preservation contract
34
+
35
+ `/qualia-vibe` ONLY changes:
36
+
37
+ - Color tokens (CSS vars / Tailwind palette / brand accents).
38
+ - Typography (font-family, scale, weights, italic accents).
39
+ - Depth tokens (shadow elevations, border treatments).
40
+ - Motion tokens (easing, duration, signature interactions).
41
+ - The "Aesthetic direction" line in DESIGN.md §1.
42
+
43
+ It does NOT touch:
44
+
45
+ - Component structure (`return (…)` JSX is preserved).
46
+ - Routing, data fetching, business logic.
47
+ - Layout grid (responsive breakpoints, container widths).
48
+ - Information architecture or copy.
49
+
50
+ If the user actually wants structural change, route to `/qualia-polish --redesign`.
51
+
52
+ ## Modes
53
+
54
+ ### Default — propose one pivot, apply it
55
+
56
+ Per `rules/one-opinion.md`. Read PRODUCT.md register + anti-references + scene sentence. Propose ONE concrete direction with one-paragraph justification. Ask one yes/no: "Ship this, or pivot?" If user pivots, ask what they didn't like, propose ONE alternative. Never widen.
57
+
58
+ ### `--variants N`
59
+
60
+ The opt-in menu. Generate N concrete direction briefs (each 4 lines: direction / color / typography / motion). Use `AskUserQuestion` to let the user pick. Default N=3, max 5.
61
+
62
+ ### `--extract <URL or image path>`
63
+
64
+ Reverse-engineer a DESIGN.md draft from a reference. Captures a screenshot at 1440 (via `scripts/playwright-capture.mjs`), runs an extract-mode vision prompt (NOT score-mode), outputs structured tokens matching DESIGN.md sections 1-7. User reviews + edits the draft before applying.
65
+
66
+ ### `--sync`
67
+
68
+ Back-sync. Reads the codebase's actual CSS vars, Tailwind config, and font imports. Diffs against DESIGN.md. Outputs (a) tokens in code not in DESIGN.md ("undocumented"), (b) tokens in DESIGN.md not in code ("orphaned"), (c) tokens that drifted in value. With `--sync --write`, patches DESIGN.md to match code.
69
+
70
+ ## Process
71
+
72
+ ### 0. Pre-flight gates
73
+
74
+ ```bash
75
+ node ~/.claude/bin/qualia-ui.js banner vibe 2>/dev/null
76
+
77
+ # DESIGN.md must exist
78
+ test -f .planning/DESIGN.md || {
79
+ echo "DESIGN.md not found. Run /qualia-new (or /qualia-polish --redesign for a ground-up redesign) first."
80
+ exit 1
81
+ }
82
+
83
+ # PRODUCT.md should exist (for register + anti-references context)
84
+ test -f .planning/PRODUCT.md || echo "Warning: PRODUCT.md missing — vibe proposals will be less anchored. Consider /qualia-new first."
85
+ ```
86
+
87
+ ### 1. Parse the mode
88
+
89
+ - `--extract <url-or-path>` → go to Extract flow.
90
+ - `--sync` (with optional `--write`) → go to Sync flow.
91
+ - `--variants [N]` → go to Variants flow.
92
+ - Direction explicitly named on command line (e.g. `/qualia-vibe brutalist`) → skip proposal, jump to Apply.
93
+ - Otherwise → Default (propose one).
94
+
95
+ ### 2. Default flow (propose one)
96
+
97
+ Read substrate:
98
+ - `.planning/PRODUCT.md` (register, anti-references, scene sentence)
99
+ - `.planning/DESIGN.md` (current direction, current token set)
100
+ - `qualia-design/design-laws.md` (banned patterns)
101
+ - `qualia-design/design-brand.md` OR `qualia-design/design-product.md` (matching register)
102
+
103
+ Propose ONE direction. Output format:
104
+
105
+ ```
106
+ Current: {DESIGN.md §1 direction line}
107
+ Proposed: {ONE new direction, concrete — e.g. "editorial broadsheet, ivory ground, deep navy ink, italic Cormorant accents"}
108
+ Why: {1-2 sentences citing PRODUCT.md register, anti-refs, scene sentence}
109
+ Token deltas: {3-7 bullet token changes the pivot implies}
110
+ Cost: ~3 min to apply, layout preserved.
111
+ ```
112
+
113
+ Use `AskUserQuestion` with two options: `Ship this pivot` / `Different direction`.
114
+
115
+ If "Different direction": ask one follow-up — "What about the proposal didn't fit?" — then propose ONE new direction with the rejection incorporated. Never enumerate alternatives.
116
+
117
+ ### 3. Apply
118
+
119
+ Once user approves a direction:
120
+
121
+ 1. Update DESIGN.md §1 (Aesthetic direction).
122
+ 2. Update DESIGN.md §2 (Color — OKLCH token set).
123
+ 3. Update DESIGN.md §3 (Typography — font-family, scale).
124
+ 4. Update DESIGN.md §6 (Depth — shadow elevations).
125
+ 5. Update DESIGN.md §7 (Motion — easing, duration).
126
+ 6. Apply the same token changes to code:
127
+ - CSS vars in `app/globals.css` or `src/styles/globals.css` (whichever exists)
128
+ - `tailwind.config.{ts,js,mjs}` colors / fonts / spacing extensions
129
+ - Font imports (`@import url(...)` in CSS, or `<link>` in `app/layout.tsx`, or `next/font/google` calls)
130
+ 7. Run `node ~/.claude/bin/slop-detect.mjs` on changed files. Block on critical findings (no banned fonts in the new vibe).
131
+ 8. Capture one screenshot at desktop (1440) for visual diff:
132
+ ```bash
133
+ node ~/.claude/skills/qualia-polish/scripts/playwright-capture.mjs \
134
+ --url ${URL:-http://localhost:3000} \
135
+ --out .planning/vibe-after.png \
136
+ --width 1440
137
+ ```
138
+ 9. Commit:
139
+ ```bash
140
+ git -c user.name="Qualia Vibe" -c user.email="vibe@qualia.solutions" \
141
+ commit -m "vibe(pivot): {old direction} → {new direction}"
142
+ ```
143
+
144
+ If the commit fails (no dev server, no slop-detect, network error), surface the exact failure — do NOT silently swallow.
145
+
146
+ ### 4. Variants flow
147
+
148
+ ```bash
149
+ node ~/.claude/skills/qualia-vibe/scripts/tokens.mjs propose-variants \
150
+ --product .planning/PRODUCT.md \
151
+ --design .planning/DESIGN.md \
152
+ --count ${N:-3}
153
+ ```
154
+
155
+ Output N briefs side-by-side. `AskUserQuestion` with N options (one per variant) plus "None of these — propose something else". User picks one → continue to Apply (step 3).
156
+
157
+ ### 5. Extract flow
158
+
159
+ ```bash
160
+ node ~/.claude/skills/qualia-vibe/scripts/extract.mjs \
161
+ --source ${URL_OR_IMAGE} \
162
+ --out .planning/DESIGN-extracted.md
163
+ ```
164
+
165
+ The script:
166
+ 1. If source is a URL, capture screenshot via `playwright-capture.mjs`.
167
+ 2. Sends screenshot + extract-mode prompt to visual-evaluator agent.
168
+ 3. Visual evaluator returns a JSON token bundle (color OKLCH, font families, scale, depth, motion).
169
+ 4. Script renders the bundle into a DESIGN.md draft.
170
+
171
+ Present the draft. Diff against current DESIGN.md. User can: `Apply as new vibe`, `Save draft only`, or `Cancel`. If apply → Apply flow (step 3) using the extracted tokens.
172
+
173
+ ### 6. Sync flow
174
+
175
+ ```bash
176
+ node ~/.claude/skills/qualia-vibe/scripts/tokens.mjs sync \
177
+ --design .planning/DESIGN.md \
178
+ ${WRITE:+--write}
179
+ ```
180
+
181
+ The script:
182
+ 1. Greps CSS for `:root { --token: value; }` declarations.
183
+ 2. Reads `tailwind.config.*` for colors / fonts / spacing extensions.
184
+ 3. Parses font imports.
185
+ 4. Diffs against DESIGN.md token sections.
186
+ 5. Prints three sections: `Undocumented (in code, not in DESIGN.md)`, `Orphaned (in DESIGN.md, not in code)`, `Drifted (different values)`.
187
+ 6. If `--write`, patches DESIGN.md to match code; commits with `vibe(sync): align DESIGN.md to code`.
188
+
189
+ ## Output contracts
190
+
191
+ Vibe always writes ONE of these as its final line:
192
+
193
+ - `DONE — vibe pivot: {old direction} → {new direction} ({sha})`
194
+ - `DONE — vibe extract: draft saved to {path}`
195
+ - `DONE — vibe sync: {N} drift findings ({sha if --write})`
196
+ - `CANCELLED — user declined all proposed pivots`
197
+ - `BLOCKED — {reason}` (DESIGN.md missing, slop-detect critical, etc.)
198
+
199
+ ## Rules
200
+
201
+ 1. **One opinion by default.** Per `rules/one-opinion.md`. Menus require explicit `--variants`.
202
+ 2. **Layout never changes.** If the proposal would require touching JSX structure, route to `/qualia-polish --redesign` and stop.
203
+ 3. **DESIGN.md and code stay in sync.** Every apply writes BOTH. Sync mode exists to recover from drift, not to normalize it.
204
+ 4. **slop-detect is the brake.** A pivot that introduces a banned font / gradient / hex-in-jsx is rejected. The new vibe must pass the same gates as the old one.
205
+ 5. **One screenshot, not three.** Vibe is fast by design. The full 3-viewport check belongs in `/qualia-polish --loop` after the vibe lands.
206
+ 6. **Commit identity is local.** Vibe sets git user inline so it works on fresh clones without global config.
207
+
208
+ ## Anti-patterns
209
+
210
+ - ❌ Presenting "Here are 5 directions: pick one" when the user said "change the vibe". → Use `rules/one-opinion.md`: propose ONE.
211
+ - ❌ Touching `return (…)` JSX. → If the pivot needs structural change, stop and route to `--redesign`.
212
+ - ❌ Skipping slop-detect because "the user is in a hurry". → The vibe pivot is exactly when banned fonts/gradients sneak in.
213
+ - ❌ Auto-running `--sync --write` without surfacing the diff first. → Always show drift before patching DESIGN.md.
214
+ - ❌ Treating `--extract` output as ground truth. → It's a DRAFT. User must approve before apply.
215
+
216
+ ## Examples
217
+
218
+ ```
219
+ /qualia-vibe # propose one pivot, apply on approval
220
+ /qualia-vibe brutalist # explicit pivot to named direction
221
+ /qualia-vibe --variants 3 # generate 3 options (use sparingly)
222
+ /qualia-vibe --extract https://stripe.com
223
+ /qualia-vibe --extract ./refs/inspo.png
224
+ /qualia-vibe --sync # show drift between code and DESIGN.md
225
+ /qualia-vibe --sync --write # patch DESIGN.md to match code, commit
226
+ ```
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * extract.mjs — reverse-engineer a DESIGN.md draft from a URL or screenshot.
4
+ *
5
+ * Pipeline:
6
+ * 1. If source is a URL → capture screenshot at 1440 via playwright-capture.mjs.
7
+ * If source is a local image path → use it directly.
8
+ * 2. Emit a JSON scaffold the LLM uses to generate the extracted token bundle.
9
+ * 3. The skill (qualia-vibe) reads the scaffold, runs the vision evaluator in
10
+ * extract mode, gets the bundle back, renders it as a DESIGN.md draft.
11
+ *
12
+ * This script does NOT call any LLM directly — it stages the inputs and emits
13
+ * a deterministic JSON contract. The /qualia-vibe skill orchestrates the LLM
14
+ * call.
15
+ *
16
+ * Usage:
17
+ * extract.mjs --source <URL or image path> [--out <path>]
18
+ *
19
+ * Exit codes:
20
+ * 0 success — JSON scaffold emitted to stdout, screenshot path included
21
+ * 1 capture failed (network, no playwright, bad URL)
22
+ * 2 invocation error
23
+ */
24
+
25
+ import { existsSync, mkdirSync, statSync } from "node:fs";
26
+ import { spawnSync } from "node:child_process";
27
+ import { argv, exit, env } from "node:process";
28
+ import { dirname, join, resolve } from "node:path";
29
+ import { tmpdir } from "node:os";
30
+
31
+ function flag(name, fallback) {
32
+ const i = argv.indexOf(name);
33
+ if (i < 0) return fallback;
34
+ return argv[i + 1] || fallback;
35
+ }
36
+
37
+ const source = flag("--source");
38
+ const outDraft = flag("--out", ".planning/DESIGN-extracted.md");
39
+
40
+ if (!source) {
41
+ console.error("--source required (URL or local image path)");
42
+ exit(2);
43
+ }
44
+
45
+ // ─── Stage 1: locate or capture screenshot ────────────────────────────
46
+
47
+ let screenshotPath;
48
+
49
+ const isUrl = /^https?:\/\//i.test(source);
50
+ if (isUrl) {
51
+ const stamp = Date.now().toString(36);
52
+ const outDir = join(tmpdir(), `qualia-vibe-extract-${stamp}`);
53
+ mkdirSync(outDir, { recursive: true });
54
+ screenshotPath = join(outDir, "ref-1440.png");
55
+
56
+ // Resolve playwright-capture.mjs — search the same order as loop.mjs.
57
+ const candidates = [
58
+ env.QUALIA_CAPTURE_SCRIPT,
59
+ `${env.HOME}/.claude/skills/qualia-polish/scripts/playwright-capture.mjs`,
60
+ resolve(dirname(new URL(import.meta.url).pathname), "..", "..", "qualia-polish", "scripts", "playwright-capture.mjs"),
61
+ ].filter(Boolean);
62
+ const captureScript = candidates.find((p) => existsSync(p));
63
+ if (!captureScript) {
64
+ console.error("playwright-capture.mjs not found. Install the framework or set QUALIA_CAPTURE_SCRIPT.");
65
+ exit(1);
66
+ }
67
+
68
+ const r = spawnSync("node", [
69
+ captureScript,
70
+ "--url", source,
71
+ "--out", screenshotPath,
72
+ "--width", "1440",
73
+ ], { encoding: "utf8" });
74
+
75
+ if (r.status !== 0 || !existsSync(screenshotPath)) {
76
+ console.error(`screenshot capture failed (exit=${r.status})`);
77
+ if (r.stderr) console.error(r.stderr);
78
+ exit(1);
79
+ }
80
+ } else {
81
+ screenshotPath = resolve(source);
82
+ if (!existsSync(screenshotPath)) {
83
+ console.error(`image not found: ${screenshotPath}`);
84
+ exit(2);
85
+ }
86
+ try { statSync(screenshotPath).isFile(); } catch {
87
+ console.error(`source must be a file: ${screenshotPath}`);
88
+ exit(2);
89
+ }
90
+ }
91
+
92
+ // ─── Stage 2: emit extraction scaffold ────────────────────────────────
93
+
94
+ const scaffold = {
95
+ mode: "extract",
96
+ source: source,
97
+ screenshot: screenshotPath,
98
+ output_draft: outDraft,
99
+ instruction:
100
+ "Examine the screenshot and EXTRACT the visible design tokens. Do NOT score, judge, or critique — describe what is actually present. Output the bundle matching the schema below. If a value is not visible (e.g. motion in a static screenshot), set it to null.",
101
+ schema: {
102
+ aesthetic_direction: "1 sentence — name the aesthetic in concrete terms",
103
+ color: {
104
+ strategy: "Restrained | Committed | Full palette | Drenched",
105
+ ground: "OKLCH or hex of the dominant background",
106
+ ink: "OKLCH or hex of the dominant text color",
107
+ accent: "OKLCH or hex of the single brand accent (if present)",
108
+ notes: "1 sentence on color logic visible (e.g. 'single saturated accent, otherwise neutrals')",
109
+ },
110
+ typography: {
111
+ primary_family: "best guess at the headline font family",
112
+ secondary_family: "best guess at body / sans counterpart, or null",
113
+ scale_observation: "tight | airy | dramatic | flat — one word",
114
+ weight_range: "e.g. '400/600' or 'mono single weight'",
115
+ italic_usage: "common | rare | never",
116
+ },
117
+ spatial: {
118
+ grid_observation: "8px | 12-col | bespoke | dense | airy — one phrase",
119
+ max_width_observation: "narrow | medium | wide | full",
120
+ },
121
+ depth: {
122
+ shadow_intensity: "none | subtle | bold",
123
+ borders: "hairline | medium | heavy | none",
124
+ },
125
+ motion: {
126
+ visible: "true | false | unknown (static)",
127
+ character: "snap | glide | spring | none",
128
+ },
129
+ register_guess: "Brand | Product | hybrid",
130
+ confidence: "low | medium | high",
131
+ },
132
+ rules: [
133
+ "Banned fonts: Inter, Roboto, Arial, Helvetica, system-ui, Space Grotesk, Montserrat, Poppins, Lato, Open Sans. If you see one of these, name it AND flag it so the user can decide whether to ban or accept.",
134
+ "Banned patterns: purple-blue gradient, gradient text, bounce/elastic easing. Flag the same way.",
135
+ "Confidence < high → user must review before /qualia-vibe applies.",
136
+ ],
137
+ next_step: `After producing the bundle, write a DESIGN.md draft to ${outDraft} using the bundle to populate sections 1 (Direction), 2 (Color), 3 (Typography), 4 (Spacing), 6 (Depth), 7 (Motion). Leave sections that depend on PRODUCT.md or anti-references empty — the user will fill them.`,
138
+ };
139
+
140
+ console.log(JSON.stringify(scaffold, null, 2));
141
+ exit(0);