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.
- package/AGENTS.md +2 -1
- package/CLAUDE.md +2 -1
- package/README.md +14 -7
- package/agents/builder.md +1 -5
- package/agents/plan-checker.md +1 -1
- package/agents/planner.md +2 -6
- package/agents/qa-browser.md +3 -3
- package/agents/roadmapper.md +1 -1
- package/agents/verifier.md +7 -9
- package/agents/visual-evaluator.md +1 -3
- package/bin/cli.js +32 -6
- package/bin/slop-detect.mjs +81 -9
- package/docs/archive/CHANGELOG-pre-v4.md +855 -0
- package/docs/onboarding.html +2 -2
- package/guide.md +15 -2
- package/hooks/auto-update.js +6 -3
- package/hooks/env-empty-guard.js +5 -4
- package/hooks/pre-compact.js +5 -3
- package/hooks/pre-push.js +57 -0
- package/package.json +2 -2
- package/qualia-design/design-reference.md +2 -1
- package/qualia-design/frontend.md +4 -4
- package/rules/one-opinion.md +59 -0
- package/rules/trust-boundary.md +35 -0
- package/skills/qualia-feature/SKILL.md +5 -5
- package/skills/qualia-flush/SKILL.md +5 -7
- package/skills/qualia-hook-gen/SKILL.md +1 -1
- package/skills/qualia-learn/SKILL.md +1 -0
- package/skills/qualia-map/SKILL.md +1 -0
- package/skills/qualia-milestone/SKILL.md +1 -1
- package/skills/qualia-new/SKILL.md +6 -6
- package/skills/qualia-plan/SKILL.md +1 -1
- package/skills/qualia-polish/REFERENCE.md +8 -6
- package/skills/qualia-polish/SKILL.md +9 -7
- package/skills/qualia-polish/scripts/loop.mjs +18 -6
- package/skills/qualia-postmortem/SKILL.md +1 -1
- package/skills/qualia-report/SKILL.md +2 -1
- package/skills/qualia-road/SKILL.md +16 -4
- package/skills/qualia-verify/SKILL.md +2 -2
- package/skills/qualia-vibe/SKILL.md +226 -0
- package/skills/qualia-vibe/scripts/extract.mjs +141 -0
- package/skills/qualia-vibe/scripts/tokens.mjs +342 -0
- package/templates/help.html +9 -2
- package/tests/bin.test.sh +12 -12
- package/tests/refs.test.sh +1 -1
- package/tests/run-all.sh +48 -0
- 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
|
|
208
|
-
|
|
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", [
|
|
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-
|
|
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
|
|
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
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
|
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).
|
|
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
|
|
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);
|