@hecer/yoke 0.2.0 → 0.7.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 (84) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +557 -494
  3. package/bench/.runs/claude-2026-07-09T22-34-01/.yoke/config.yaml +6 -0
  4. package/bench/.runs/claude-2026-07-09T22-34-01/.yoke/context/DECISIONS.md +9 -0
  5. package/bench/.runs/claude-2026-07-09T22-34-01/.yoke/prd.yaml +38 -0
  6. package/bench/.runs/claude-2026-07-09T22-34-01/bench-verify.mjs +15 -0
  7. package/bench/.runs/claude-2026-07-09T22-34-01/package.json +9 -0
  8. package/bench/.runs/claude-2026-07-09T22-34-01/src/index.mjs +48 -0
  9. package/bench/.runs/claude-2026-07-09T22-34-01/tests/STORY-1.test.mjs +24 -0
  10. package/bench/.runs/claude-2026-07-09T22-34-01/tests/STORY-2.test.mjs +28 -0
  11. package/bench/.runs/claude-2026-07-09T22-34-01/tests/STORY-3.test.mjs +25 -0
  12. package/bench/.runs/gemini-2026-07-09T22-34-02/.yoke/config.yaml +6 -0
  13. package/bench/.runs/gemini-2026-07-09T22-34-02/.yoke/prd.yaml +32 -0
  14. package/bench/.runs/gemini-2026-07-09T22-34-02/bench-verify.mjs +15 -0
  15. package/bench/.runs/gemini-2026-07-09T22-34-02/package.json +9 -0
  16. package/bench/.runs/gemini-2026-07-09T22-34-02/src/index.mjs +3 -0
  17. package/bench/.runs/gemini-2026-07-09T22-34-02/tests/STORY-1.test.mjs +24 -0
  18. package/bench/.runs/gemini-2026-07-09T22-34-02/tests/STORY-2.test.mjs +28 -0
  19. package/bench/.runs/gemini-2026-07-09T22-34-02/tests/STORY-3.test.mjs +25 -0
  20. package/bench/README.md +42 -0
  21. package/bench/RESULTS.md +36 -0
  22. package/bench/fixtures/string-kit/.yoke/config.yaml +6 -0
  23. package/bench/fixtures/string-kit/.yoke/prd.yaml +32 -0
  24. package/bench/fixtures/string-kit/bench-verify.mjs +15 -0
  25. package/bench/fixtures/string-kit/package.json +9 -0
  26. package/bench/fixtures/string-kit/src/index.mjs +3 -0
  27. package/bench/fixtures/string-kit/tests/STORY-1.test.mjs +24 -0
  28. package/bench/fixtures/string-kit/tests/STORY-2.test.mjs +28 -0
  29. package/bench/fixtures/string-kit/tests/STORY-3.test.mjs +25 -0
  30. package/bench/results/claude-2026-07-09T22-34-01.json +40 -0
  31. package/bench/run.mjs +115 -0
  32. package/canon/loop/loop-spec.md +5 -1
  33. package/canon/manifest.yaml +3 -0
  34. package/canon/skills/yoke-retrofit/SKILL.md +1 -0
  35. package/canon/tools/claude-mem.md +15 -0
  36. package/canon/tools/ui-ux-pro-max.md +15 -0
  37. package/dist/cli.js +32 -6
  38. package/dist/loop/cleanup.js +44 -3
  39. package/dist/loop/loop.js +27 -2
  40. package/dist/loop/reporter.js +21 -3
  41. package/dist/loop/run-command.js +19 -6
  42. package/dist/loop/runner.js +130 -13
  43. package/dist/loop/watchdog.js +67 -17
  44. package/dist/retrofit/apply.js +10 -3
  45. package/dist/retrofit/config.js +2 -0
  46. package/dist/retrofit/gitignore.js +2 -0
  47. package/dist/retrofit/planners/claude.js +4 -1
  48. package/dist/retrofit/planners/gemini.js +2 -1
  49. package/dist/retrofit/preserve.js +51 -0
  50. package/dist/update/check.js +86 -0
  51. package/dist/update/refresh.js +28 -0
  52. package/dist/update/upgrade.js +30 -0
  53. package/docs/PUBLISHING.md +41 -0
  54. package/docs/superpowers/plans/2026-06-27-forge-a-canon.md +815 -0
  55. package/docs/superpowers/plans/2026-06-27-forge-b1-retrofit-claude.md +842 -0
  56. package/docs/superpowers/plans/2026-06-27-forge-b2-codex-gemini-tools.md +884 -0
  57. package/docs/superpowers/plans/2026-06-27-forge-c1-loop-engine.md +891 -0
  58. package/docs/superpowers/plans/2026-06-28-baustein-e-context-layer.md +981 -0
  59. package/docs/superpowers/plans/2026-06-28-forge-b3-settings-merge.md +364 -0
  60. package/docs/superpowers/plans/2026-06-28-forge-c2-loop-verify.md +502 -0
  61. package/docs/superpowers/plans/2026-06-28-forge-c3-multi-agent-runners.md +377 -0
  62. package/docs/superpowers/plans/2026-06-28-forge-c4-worktree-isolation.md +413 -0
  63. package/docs/superpowers/plans/2026-06-28-forge-c5-review-iteration.md +419 -0
  64. package/docs/superpowers/plans/2026-06-28-forge-cleanup-dep0190.md +233 -0
  65. package/docs/superpowers/plans/2026-06-28-forge-d-codegraph-choice-minimal-code.md +523 -0
  66. package/docs/superpowers/plans/2026-06-29-baustein-f-routing.md +258 -0
  67. package/docs/superpowers/plans/2026-06-29-baustein-g-loop-observability.md +1006 -0
  68. package/docs/superpowers/plans/2026-06-29-baustein-h-loop-robustness.md +374 -0
  69. package/docs/superpowers/plans/2026-06-30-baustein-i-visual-design-verification.md +450 -0
  70. package/docs/superpowers/plans/2026-07-02-baustein-j-cross-model-review-gstack-compose.md +645 -0
  71. package/docs/superpowers/plans/2026-07-02-baustein-k-zero-to-100-bootstrap.md +1024 -0
  72. package/docs/superpowers/plans/2026-07-02-baustein-m-flow-smoke-proofs.md +574 -0
  73. package/docs/superpowers/specs/2026-06-27-forge-cross-agent-harness-design.md +144 -0
  74. package/docs/superpowers/specs/2026-06-28-baustein-e-context-layer-design.md +146 -0
  75. package/docs/superpowers/specs/2026-06-29-baustein-f-routing-design.md +106 -0
  76. package/docs/superpowers/specs/2026-06-29-baustein-g-loop-observability-design.md +186 -0
  77. package/docs/superpowers/specs/2026-06-29-baustein-h-loop-robustness-design.md +113 -0
  78. package/docs/superpowers/specs/2026-06-30-baustein-i-visual-design-verification-design.md +98 -0
  79. package/docs/superpowers/specs/2026-07-02-baustein-j-cross-model-review-gstack-compose-design.md +137 -0
  80. package/docs/superpowers/specs/2026-07-02-baustein-k-zero-to-100-bootstrap-design.md +200 -0
  81. package/docs/superpowers/specs/2026-07-02-baustein-m-flow-smoke-proofs-design.md +155 -0
  82. package/docs/superpowers/specs/2026-07-10-companion-tools-decision.md +33 -0
  83. package/docs/superpowers/specs/2026-07-10-multi-agent-parallel-loop-design.md +99 -0
  84. package/package.json +8 -2
@@ -0,0 +1,450 @@
1
+ # Baustein I — Visual & Design Verification Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Add a static AI-slop **design scanner** (`yoke design-scan`, gated on exit code) plus two canon skills (`unslop-ui`, `visual-verification`) so the loop's verify gate can cover visual/design quality and user-flow smoke, not just unit tests.
6
+
7
+ **Architecture:** A pure `src/scan/design.ts` (curated tell set + `scanText`/`scanDir`) wired to a `yoke design-scan` CLI command (`runDesignScan`), plus markdown canon skills + manifest/attribution/README. No browser is embedded — flow-smoke/video is methodology in the `visual-verification` skill using the already-wired Playwright MCP.
8
+
9
+ **Tech Stack:** Node.js + TypeScript (ESM, `.js` specifiers, strict), vitest. `npx vitest run`, `npx tsc --noEmit`.
10
+
11
+ ---
12
+
13
+ ## File Structure
14
+
15
+ | File | Change |
16
+ |------|--------|
17
+ | `src/scan/design.ts` (create) | `Tell`/`Finding`/`ScanResult` types, `TELLS`, `scanText`, `scanDir` |
18
+ | `src/cli.ts` (modify) | `runDesignScan` + `design-scan` case + usage |
19
+ | `canon/skills/unslop-ui/SKILL.md` (create) | design-tells rubric |
20
+ | `canon/skills/visual-verification/SKILL.md` (create) | verify-pipeline + flow-smoke + video-on-failure |
21
+ | `canon/manifest.yaml` (modify) | register both skills |
22
+ | `canon/skills/ATTRIBUTION.md` (modify) | credit vibecoded-design-tells (MIT) |
23
+ | `tests/scan/design.test.ts`, `tests/scan/design-cli.test.ts` (create) | scanner + CLI tests |
24
+ | `tests/canon/real-canon.test.ts` (modify) | assert both skills registered |
25
+ | `README.md` (modify) | Visual-verification section + catalog 24→26 + badge sync |
26
+
27
+ ---
28
+
29
+ ### Task 1: the static scanner — `src/scan/design.ts`
30
+
31
+ **Files:** Create `src/scan/design.ts`; Test `tests/scan/design.test.ts`
32
+
33
+ - [ ] **Step 1: Write the failing test**
34
+
35
+ ```typescript
36
+ // tests/scan/design.test.ts
37
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
38
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
39
+ import { join } from 'node:path'
40
+ import { tmpdir } from 'node:os'
41
+ import { scanText, scanDir, TELLS } from '../../src/scan/design.js'
42
+
43
+ describe('scanText', () => {
44
+ it('flags AI-purple hex and tailwind purple gradients', () => {
45
+ expect(scanText('color: #6c5ce7;').some(m => m.tell.name === 'ai-purple')).toBe(true)
46
+ expect(scanText('<div class="bg-gradient-to-r from-purple-500 to-violet-600">').some(m => m.tell.name === 'ai-purple')).toBe(true)
47
+ })
48
+ it('flags gradient hero text (clip-text + transparent)', () => {
49
+ expect(scanText('<h1 class="bg-clip-text text-transparent bg-gradient-to-r">').some(m => m.tell.name === 'gradient-clip-text')).toBe(true)
50
+ })
51
+ it('flags neon glow', () => {
52
+ expect(scanText('class="shadow-[0_0_20px_rgba(0,255,255,0.7)]"').some(m => m.tell.name === 'neon-glow')).toBe(true)
53
+ expect(scanText('box-shadow: 0 0 40px #0ff;').some(m => m.tell.name === 'neon-glow')).toBe(true)
54
+ })
55
+ it('flags gradient overload', () => {
56
+ expect(scanText('background: linear-gradient(90deg, #fff, #000);').some(m => m.tell.name === 'gradient-overload')).toBe(true)
57
+ })
58
+ it('does not flag clean, conventional styles', () => {
59
+ expect(scanText('class="rounded-lg border bg-white p-4 text-slate-800"')).toEqual([])
60
+ expect(scanText('color: #1d1d1f; background: #f5f5f7;')).toEqual([])
61
+ })
62
+ it('counts at most one finding per tell per line', () => {
63
+ const m = scanText('from-purple-500 via-purple-600 to-purple-700')
64
+ expect(m.filter(x => x.tell.name === 'ai-purple')).toHaveLength(1)
65
+ })
66
+ })
67
+
68
+ describe('scanDir', () => {
69
+ let dir: string
70
+ beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'yoke-scan-')) })
71
+ afterEach(() => { rmSync(dir, { recursive: true, force: true }) })
72
+
73
+ it('walks source files, aggregates a weighted score, and records file:line', () => {
74
+ writeFileSync(join(dir, 'a.css'), 'h1 { color: #6c5ce7; }\n') // ai-purple weight 2
75
+ writeFileSync(join(dir, 'b.tsx'), '\n<span class="shadow-[0_0_10px_#f0f]" />') // neon-glow weight 2, line 2
76
+ const res = scanDir(dir)
77
+ expect(res.score).toBe(4)
78
+ expect(res.findings.some(f => f.file.endsWith('a.css') && f.line === 1 && f.tell === 'ai-purple')).toBe(true)
79
+ expect(res.findings.some(f => f.file.endsWith('b.tsx') && f.line === 2 && f.tell === 'neon-glow')).toBe(true)
80
+ })
81
+ it('skips node_modules / dist / .yoke and non-source extensions', () => {
82
+ mkdirSync(join(dir, 'node_modules'), { recursive: true })
83
+ writeFileSync(join(dir, 'node_modules', 'x.css'), 'color:#6c5ce7;')
84
+ writeFileSync(join(dir, 'readme.md'), 'a #6c5ce7 mention')
85
+ expect(scanDir(dir).score).toBe(0)
86
+ })
87
+ it('accepts an injected tell set', () => {
88
+ writeFileSync(join(dir, 'a.css'), 'foo')
89
+ const tells = [{ name: 'foo', weight: 5, test: (l: string) => l.includes('foo'), hint: 'h' }]
90
+ expect(scanDir(dir, tells).score).toBe(5)
91
+ })
92
+ })
93
+
94
+ it('TELLS is the curated non-empty default set', () => {
95
+ expect(TELLS.map(t => t.name)).toEqual(
96
+ expect.arrayContaining(['ai-purple', 'gradient-clip-text', 'neon-glow', 'gradient-overload']),
97
+ )
98
+ })
99
+ ```
100
+
101
+ - [ ] **Step 2: Run it to verify it fails**
102
+
103
+ Run: `npx vitest run tests/scan/design.test.ts`
104
+ Expected: FAIL — module missing.
105
+
106
+ - [ ] **Step 3: Implement** `src/scan/design.ts`
107
+
108
+ ```typescript
109
+ import { readdirSync, readFileSync, statSync } from 'node:fs'
110
+ import { join, extname } from 'node:path'
111
+
112
+ export interface Tell {
113
+ name: string
114
+ weight: number
115
+ test: (line: string) => boolean
116
+ hint: string
117
+ }
118
+ export interface Match { line: number; tell: Tell; text: string }
119
+ export interface Finding { file: string; line: number; tell: string; hint: string; text: string }
120
+ export interface ScanResult { findings: Finding[]; score: number }
121
+
122
+ const AI_PURPLE_HEX = /#(6c5ce7|7c3aed|8b5cf6|a855f7|9333ea)\b/i
123
+ const AI_PURPLE_TW = /\b(from|via|to)-(purple|violet|fuchsia)-(4|5|6|7)00\b/i
124
+ const NEON_TW = /\b(shadow|drop-shadow)-\[0_0_/i
125
+ const NEON_CSS = /box-shadow:[^;]*\b0\s+0\s+\d{2,}px/i
126
+ const EMOJI = /\p{Extended_Pictographic}/u
127
+ const JSX_ICON_CTX = /<button|<a\s|aria-hidden|(icon|emoji)/i
128
+
129
+ export const TELLS: Tell[] = [
130
+ { name: 'ai-purple', weight: 2, hint: 'AI-purple is the #1 vibecoded tell — pick a real brand color',
131
+ test: (l) => AI_PURPLE_HEX.test(l) || AI_PURPLE_TW.test(l) },
132
+ { name: 'gradient-clip-text', weight: 2, hint: 'Gradient hero text reads as AI-slop — use a solid color + weight',
133
+ test: (l) => (/bg-clip-text/.test(l) && /text-transparent/.test(l)) || /-webkit-background-clip:\s*text/i.test(l) },
134
+ { name: 'neon-glow', weight: 2, hint: 'Neon glow is a tell — use subtle, neutral elevation',
135
+ test: (l) => NEON_TW.test(l) || NEON_CSS.test(l) },
136
+ { name: 'gradient-overload', weight: 1, hint: 'Gradients everywhere flatten hierarchy — use them sparingly',
137
+ test: (l) => /bg-gradient-to-/.test(l) || /linear-gradient\(/i.test(l) },
138
+ { name: 'emoji-icon', weight: 1, hint: 'Emoji-as-icons is a tell — use a real icon set',
139
+ test: (l) => EMOJI.test(l) && JSX_ICON_CTX.test(l) },
140
+ ]
141
+
142
+ // One match per (line, tell) at most, so a line with three purple classes counts once.
143
+ export function scanText(text: string, tells: Tell[] = TELLS): Match[] {
144
+ const matches: Match[] = []
145
+ const lines = text.split(/\r?\n/)
146
+ lines.forEach((line, i) => {
147
+ for (const tell of tells) {
148
+ if (tell.test(line)) matches.push({ line: i + 1, tell, text: line.trim().slice(0, 200) })
149
+ }
150
+ })
151
+ return matches
152
+ }
153
+
154
+ const EXT = new Set(['.css', '.scss', '.tsx', '.jsx', '.ts', '.js', '.html', '.vue', '.svelte', '.astro'])
155
+ const SKIP = new Set(['node_modules', 'dist', '.next', 'build', '.yoke', 'coverage', '.git', 'out'])
156
+
157
+ function walk(dir: string, acc: string[]): void {
158
+ for (const entry of readdirSync(dir)) {
159
+ const full = join(dir, entry)
160
+ let s
161
+ try { s = statSync(full) } catch { continue }
162
+ if (s.isDirectory()) {
163
+ if (!SKIP.has(entry)) walk(full, acc)
164
+ } else if (EXT.has(extname(entry).toLowerCase())) {
165
+ acc.push(full)
166
+ }
167
+ }
168
+ }
169
+
170
+ export function scanDir(dir: string, tells: Tell[] = TELLS): ScanResult {
171
+ const files: string[] = []
172
+ walk(dir, files)
173
+ const findings: Finding[] = []
174
+ let score = 0
175
+ for (const file of files) {
176
+ let text: string
177
+ try { text = readFileSync(file, 'utf8') } catch { continue }
178
+ for (const m of scanText(text, tells)) {
179
+ findings.push({ file, line: m.line, tell: m.tell.name, hint: m.tell.hint, text: m.text })
180
+ score += m.tell.weight
181
+ }
182
+ }
183
+ return { findings, score }
184
+ }
185
+ ```
186
+
187
+ - [ ] **Step 4: Run it green** — `npx vitest run tests/scan/design.test.ts` → PASS.
188
+
189
+ - [ ] **Step 5: Commit**
190
+ ```bash
191
+ git add src/scan/design.ts tests/scan/design.test.ts
192
+ git commit -m "feat(scan): static AI-slop design-tell scanner (scanText/scanDir)"
193
+ ```
194
+
195
+ ---
196
+
197
+ ### Task 2: `yoke design-scan` CLI
198
+
199
+ **Files:** Modify `src/cli.ts`; Test `tests/scan/design-cli.test.ts`
200
+
201
+ - [ ] **Step 1: Write the failing test**
202
+
203
+ ```typescript
204
+ // tests/scan/design-cli.test.ts
205
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
206
+ import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'
207
+ import { join } from 'node:path'
208
+ import { tmpdir } from 'node:os'
209
+ import { runDesignScan } from '../../src/cli.js'
210
+
211
+ let dir: string
212
+ beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'yoke-dscli-')) })
213
+ afterEach(() => { rmSync(dir, { recursive: true, force: true }) })
214
+
215
+ describe('runDesignScan', () => {
216
+ it('exits 0 when the slop score is within --max', () => {
217
+ writeFileSync(join(dir, 'a.css'), 'color: #6c5ce7;') // score 2
218
+ expect(runDesignScan(dir, { max: 4, report: false })).toBe(0)
219
+ })
220
+ it('exits 1 when the slop score exceeds --max', () => {
221
+ writeFileSync(join(dir, 'a.css'), 'color:#6c5ce7; box-shadow: 0 0 40px #0ff; background: linear-gradient(#fff,#000);') // 2+2+1 = 5
222
+ expect(runDesignScan(dir, { max: 4, report: false })).toBe(1)
223
+ })
224
+ it('always exits 0 in --report mode even with findings', () => {
225
+ writeFileSync(join(dir, 'a.css'), 'color:#6c5ce7; box-shadow: 0 0 40px #0ff;')
226
+ expect(runDesignScan(dir, { max: 0, report: true })).toBe(0)
227
+ })
228
+ it('exits 0 on a clean project', () => {
229
+ writeFileSync(join(dir, 'a.css'), 'color:#1d1d1f;')
230
+ expect(runDesignScan(dir, { max: 4, report: false })).toBe(0)
231
+ })
232
+ })
233
+ ```
234
+
235
+ - [ ] **Step 2: Run it to verify it fails** — `npx vitest run tests/scan/design-cli.test.ts` → FAIL (`runDesignScan` not exported).
236
+
237
+ - [ ] **Step 3: Implement** in `src/cli.ts`
238
+
239
+ Add the import (with the other imports):
240
+ ```typescript
241
+ import { scanDir } from './scan/design.js'
242
+ ```
243
+ Add the exported function (near `runValidate`):
244
+ ```typescript
245
+ export function runDesignScan(targetDir: string, opts: { max: number; report: boolean }): number {
246
+ const { findings, score } = scanDir(targetDir)
247
+ for (const f of findings) {
248
+ console.log(` ${f.file}:${f.line} ${f.tell} — ${f.hint}`)
249
+ }
250
+ const label = `Design scan: score ${score} (${findings.length} tell${findings.length === 1 ? '' : 's'}), budget ${opts.max}`
251
+ if (opts.report) { console.log(`${label} — report only`); return 0 }
252
+ if (score > opts.max) { console.log(`${label} — ✗ over budget`); return 1 }
253
+ console.log(`${label} — ✓`)
254
+ return 0
255
+ }
256
+ ```
257
+ Add a `case` in `main`'s switch (before `default`):
258
+ ```typescript
259
+ case 'design-scan': {
260
+ const targetDir = rest.find(a => !a.startsWith('-')) ?? '.'
261
+ const report = rest.includes('--report')
262
+ const maxArg = rest.find(a => a.startsWith('--max='))
263
+ const max = maxArg ? Number(maxArg.slice('--max='.length)) : 4
264
+ if (!Number.isFinite(max) || max < 0) { console.error(`Invalid --max value: ${maxArg}`); return 1 }
265
+ return runDesignScan(targetDir, { max, report })
266
+ }
267
+ ```
268
+ Append ` | design-scan [dir] [--max=N] [--report]` to the top-level `default` usage string.
269
+
270
+ - [ ] **Step 4: Run green + full suite + types** — `npx vitest run && npx tsc --noEmit` → PASS, clean.
271
+
272
+ - [ ] **Step 5: Commit**
273
+ ```bash
274
+ git add src/cli.ts tests/scan/design-cli.test.ts
275
+ git commit -m "feat(cli): yoke design-scan — gate on AI-slop design score"
276
+ ```
277
+
278
+ ---
279
+
280
+ ### Task 3: canon skills + manifest + attribution
281
+
282
+ **Files:** Create `canon/skills/unslop-ui/SKILL.md`, `canon/skills/visual-verification/SKILL.md`; Modify `canon/manifest.yaml`, `canon/skills/ATTRIBUTION.md`; Test `tests/canon/real-canon.test.ts`
283
+
284
+ - [ ] **Step 1: Write the failing test** — add to `tests/canon/real-canon.test.ts` (inside `describe('real canon')`):
285
+ ```typescript
286
+ it('registers the visual verification skills', () => {
287
+ const manifest = loadManifest(join(repoRoot, 'canon', 'manifest.yaml'))
288
+ expect(manifest.skills.some(s => s.id === 'unslop-ui')).toBe(true)
289
+ expect(manifest.skills.some(s => s.id === 'visual-verification')).toBe(true)
290
+ })
291
+ ```
292
+ Run `npx vitest run tests/canon/real-canon.test.ts` → FAIL.
293
+
294
+ - [ ] **Step 2: Create `canon/skills/unslop-ui/SKILL.md`**
295
+ ```markdown
296
+ ---
297
+ name: unslop-ui
298
+ description: Use when building or reviewing any UI — detect and remove the visual "tells" of AI-generated/vibecoded design (AI-purple gradients, gradient hero text, neon glow, emoji-as-icons, untouched shadcn defaults, centered-hero-plus-three-cards) so the result looks deliberately designed, not machine-default.
299
+ ---
300
+
301
+ # Un-slop the UI
302
+
303
+ AI-built UIs are "recognizable on sight" — a homogeneous look from a handful of default choices.
304
+ Before finishing UI work, remove these tells. Run `yoke design-scan .` for the statically
305
+ detectable ones, then apply judgement for the structural ones the scanner can't see.
306
+
307
+ ## The ranked tells (and the fix)
308
+
309
+ - **AI-purple gradients** (the #1 tell) — purple/violet gradients and `#6c5ce7`-family accents.
310
+ → Choose one real brand color with intent; avoid purple-by-default.
311
+ - **Gradient hero text** — `bg-clip-text text-transparent` rainbow headings.
312
+ → Solid color + type weight/scale for emphasis.
313
+ - **Neon glow** — `0 0 Npx` colored box-shadows / `shadow-[0_0_...]`.
314
+ → Subtle, neutral elevation (small offset, low blur, low opacity).
315
+ - **Emoji-as-icons** — 🚀✨🔥 in buttons/nav/feature lists.
316
+ → A real icon set (lucide, etc.), consistent stroke + size.
317
+ - **Untouched shadcn/Tailwind defaults** — default radius, default slate everywhere.
318
+ → Set deliberate tokens (radius, spacing scale, one accent) so it doesn't look boilerplate.
319
+ - **Centered hero + three feature cards** — the canonical AI landing layout.
320
+ → Vary rhythm: asymmetry, a real product shot, content density that fits the product.
321
+ - **Homogeneous spacing / no hierarchy** — everything the same size and gap.
322
+ → Establish a type scale and spacing rhythm; make the primary action obviously primary.
323
+
324
+ ## Rule
325
+
326
+ Treat `yoke design-scan .` as a gate (it exits non-zero over budget). Fix findings, then
327
+ eyeball the structural tells above. Distinctive, intentional > generic-but-safe.
328
+
329
+ *Rubric informed by the MIT-licensed research in [vibecoded-design-tells](https://github.com/JCarterJohnson/vibecoded-design-tells) (© Carter Johnson). Yoke implements the idea natively; no code/data copied.*
330
+ ```
331
+
332
+ - [ ] **Step 3: Create `canon/skills/visual-verification/SKILL.md`**
333
+ ```markdown
334
+ ---
335
+ name: visual-verification
336
+ description: Use for any UI/web project — make the verify gate cover more than unit tests by composing a pipeline (types → unit → design-scan → flow-smoke) and driving a Playwright flow-smoke (render + no console errors + screenshot); capture video only on failure. Catches the unwired-page / runtime-crash / AI-slop bugs unit tests miss.
337
+ ---
338
+
339
+ # Visual verification
340
+
341
+ Unit tests don't see a blank page, an unwired route, a runtime console error, or AI-slop design.
342
+ Make the loop's gate catch them by widening `verify`, since the loop trusts verify as truth.
343
+
344
+ ## 1. Compose the verify pipeline
345
+
346
+ Set `verify.command` (in `.yoke/config.yaml`) to chain, fail-fast:
347
+
348
+ ```
349
+ <typecheck> && <unit tests> && yoke design-scan . && <flow-smoke>
350
+ ```
351
+ e.g. `tsc --noEmit && vitest run && yoke design-scan . && npm run smoke`. Any red step blocks the story.
352
+
353
+ ## 2. Flow-smoke with the wired Playwright MCP
354
+
355
+ For the key user flows (home, signup/login, the primary action, checkout), against the running
356
+ dev server:
357
+ - load the route, assert it renders the expected landmark, and assert the **console has no errors**;
358
+ - take a screenshot of each for the record.
359
+
360
+ This is what catches "the page is wired wrong / it crashes on load" — the class of bug unit tests pass straight through.
361
+
362
+ ## 3. Video only when necessary
363
+
364
+ Recording + analysing video is token-heavy. Capture a video of a flow **only when a flow-smoke
365
+ fails** (or when explicitly debugging a UX problem), then analyse that clip. Never record every run.
366
+
367
+ ## Rule
368
+
369
+ Green pipeline = types + units + no design-slop over budget + key flows render without console
370
+ errors. Only then is the story actually done.
371
+ ```
372
+
373
+ - [ ] **Step 4: Register in `canon/manifest.yaml`** — add after the `workflow` entry (or with the methodology skills):
374
+ ```yaml
375
+ - { id: unslop-ui, path: skills/unslop-ui, kind: methodology }
376
+ - { id: visual-verification, path: skills/visual-verification, kind: methodology }
377
+ ```
378
+
379
+ - [ ] **Step 5: Credit in `canon/skills/ATTRIBUTION.md`** — append:
380
+ ```markdown
381
+
382
+ ## Design-tell research
383
+
384
+ `unslop-ui` and the `yoke design-scan` tell set are informed by
385
+ [vibecoded-design-tells](https://github.com/JCarterJohnson/vibecoded-design-tells)
386
+ (MIT © 2026 Carter Johnson) — a data-ranked study of AI-generated-UI tells. Yoke implements the
387
+ idea natively in TypeScript and copies no code or data.
388
+ ```
389
+
390
+ - [ ] **Step 6: Run tests** — `npx vitest run tests/canon/` → PASS (both skills registered, `validateCanon` zero-error). Confirm the new skills' single-line descriptions pass validation.
391
+
392
+ - [ ] **Step 7: Commit**
393
+ ```bash
394
+ git add canon/skills/unslop-ui canon/skills/visual-verification canon/manifest.yaml canon/skills/ATTRIBUTION.md tests/canon/real-canon.test.ts
395
+ git commit -m "feat(canon): unslop-ui + visual-verification skills (credit vibecoded-design-tells)"
396
+ ```
397
+
398
+ ---
399
+
400
+ ### Task 4: README (mandatory)
401
+
402
+ **Files:** Modify `README.md`
403
+
404
+ - [ ] **Step 1: Add a "Visual & design verification" section** after the autonomous-loop/Context-layer sections:
405
+ ```markdown
406
+ ## 🎨 Visual & design verification
407
+
408
+ Unit tests don't catch a blank page, an unwired route, or generic AI-slop design. Yoke adds two things:
409
+
410
+ - **`yoke design-scan [dir]`** — a static scanner for the visual *tells* of AI-generated UIs
411
+ (AI-purple gradients, gradient hero text, neon glow, emoji-as-icons, gradient overload). It
412
+ scores findings and **exits non-zero over budget** (`--max`, default 4; `--report` to list only),
413
+ so it drops straight into your verify pipeline.
414
+ - **`unslop-ui` + `visual-verification` skills** — the design rubric, plus how to compose a verify
415
+ pipeline (`types → units → design-scan → Playwright flow-smoke`) and capture video *only on failure*.
416
+
417
+ Because the loop trusts **verify as the source of truth**, widening `verify.command` to include the
418
+ scanner and a flow-smoke makes visual quality a real gate — not an afterthought.
419
+
420
+ *Tell set informed by the MIT-licensed [vibecoded-design-tells](https://github.com/JCarterJohnson/vibecoded-design-tells) research.*
421
+ ```
422
+
423
+ - [ ] **Step 2: Update the skills catalog** — in the "What's in the canon" section: change the heading count from **24** to **26**; change the **Process / methodology** group count from **(13)** to **(15)**; add two rows to that table:
424
+ ```markdown
425
+ | `unslop-ui` | Detect & remove AI-slop design tells (purple gradients, neon glow, emoji-icons…) |
426
+ | `visual-verification` | Widen verify to flow-smoke + design-scan; video only on failure |
427
+ ```
428
+
429
+ - [ ] **Step 3: Sync the test-count badge** — run the full suite to get the current count, then update the three `tests-NNN` references (badge line, the "✅ NNN tests" highlight, and `npm test # vitest (NNN tests)`) to the new number.
430
+
431
+ Run: `npx vitest run` → note the count; update README.
432
+
433
+ - [ ] **Step 4: Commit**
434
+ ```bash
435
+ git add README.md
436
+ git commit -m "docs: visual & design verification section + catalog 24→26 + test count"
437
+ ```
438
+
439
+ ---
440
+
441
+ ## Self-Review
442
+
443
+ **Spec coverage:** scanner (`scanText`/`scanDir`/`TELLS`) → Task 1; `yoke design-scan` gate → Task 2; `unslop-ui` + `visual-verification` skills + manifest + attribution → Task 3; README + catalog + counts → Task 4. Flow-smoke/video are methodology in `visual-verification` (no browser embedded) per the spec.
444
+
445
+ **Placeholder scan:** No TBD/TODO; every code/markdown step is complete. The tell regexes and weights are concrete and match the test expectations (ai-purple 2, neon 2, clip 2, gradient 1, emoji 1).
446
+
447
+ **Type consistency:** `Tell`/`Match`/`Finding`/`ScanResult`, `scanText(text, tells?)`, `scanDir(dir, tells?)`, `TELLS`, `runDesignScan(dir, {max, report})` are defined once and used consistently. The CLI default `--max=4` matches the spec and the CLI tests (score 5 > 4 → exit 1; score 2 ≤ 4 → exit 0).
448
+
449
+ **Counts:** manifest gains 2 skills (24→26); README heading 26 and methodology group 13→15 (the README groups by provenance — the two new Yoke-authored methodology skills go in the Process/methodology group, keeping 13+2=15 there; 15+7 roles+4 yoke-native… note: re-verify the README's own grouping math sums to 26 when editing, adjusting whichever group label the new rows live under).
450
+ ```