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
@@ -0,0 +1,342 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * tokens.mjs — read/write design tokens for /qualia-vibe.
4
+ *
5
+ * Commands:
6
+ * tokens.mjs sync --design .planning/DESIGN.md [--write]
7
+ * Diffs CSS vars + Tailwind tokens in code against DESIGN.md.
8
+ * Reports drift; with --write, patches DESIGN.md to match code.
9
+ *
10
+ * tokens.mjs propose-variants --product .planning/PRODUCT.md --design .planning/DESIGN.md --count 3
11
+ * Generates N variant direction briefs from PRODUCT.md context.
12
+ * This script only formats the prompt scaffold; the LLM does the
13
+ * actual creative work and writes the final brief.
14
+ *
15
+ * Exit codes:
16
+ * 0 success (drift report or variants emitted)
17
+ * 1 drift found AND --write not set (informational, not a hard fail)
18
+ * 2 invocation error (missing files, bad flags)
19
+ */
20
+
21
+ import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from "node:fs";
22
+ import { join, basename, extname } from "node:path";
23
+ import { argv, exit, cwd } from "node:process";
24
+
25
+ function flag(name, fallback) {
26
+ const i = argv.indexOf(name);
27
+ if (i < 0) return fallback;
28
+ const next = argv[i + 1];
29
+ if (!next || next.startsWith("--")) return true;
30
+ return next;
31
+ }
32
+ function hasFlag(name) {
33
+ return argv.includes(name);
34
+ }
35
+
36
+ const CMD = argv[2];
37
+
38
+ if (!CMD || CMD === "--help" || CMD === "-h") {
39
+ console.log(`tokens.mjs — design-token utility for /qualia-vibe
40
+
41
+ Usage:
42
+ tokens.mjs sync --design <path> [--write]
43
+ tokens.mjs propose-variants --product <path> --design <path> --count <N>
44
+
45
+ See skills/qualia-vibe/SKILL.md.
46
+ `);
47
+ exit(0);
48
+ }
49
+
50
+ // ─── CSS var extraction ───────────────────────────────────────────────
51
+
52
+ const CSS_ROOT_RE = /:root\s*\{([\s\S]*?)\}/g;
53
+ const CSS_VAR_RE = /--([a-z][a-z0-9-]*)\s*:\s*([^;]+);/gi;
54
+
55
+ function extractCssVarsFromFile(path) {
56
+ let content;
57
+ try { content = readFileSync(path, "utf8"); } catch { return []; }
58
+ const vars = [];
59
+ let m;
60
+ CSS_ROOT_RE.lastIndex = 0;
61
+ while ((m = CSS_ROOT_RE.exec(content)) !== null) {
62
+ const block = m[1];
63
+ let v;
64
+ CSS_VAR_RE.lastIndex = 0;
65
+ while ((v = CSS_VAR_RE.exec(block)) !== null) {
66
+ vars.push({ name: v[1], value: v[2].trim(), source: path });
67
+ }
68
+ }
69
+ return vars;
70
+ }
71
+
72
+ function findStyleFiles() {
73
+ const candidates = [
74
+ "app/globals.css", "app/global.css",
75
+ "src/app/globals.css", "src/app/global.css",
76
+ "src/styles/globals.css", "src/styles/global.css",
77
+ "styles/globals.css", "styles/global.css",
78
+ ];
79
+ return candidates.filter((p) => existsSync(p));
80
+ }
81
+
82
+ function findTailwindConfig() {
83
+ for (const name of ["tailwind.config.ts", "tailwind.config.js", "tailwind.config.mjs", "tailwind.config.cjs"]) {
84
+ if (existsSync(name)) return name;
85
+ }
86
+ return null;
87
+ }
88
+
89
+ function extractTailwindTokens(configPath) {
90
+ // Cheap parse: grep for color/font/spacing entries inside theme.extend.
91
+ // Not a real JS parser — but it catches the common cases without sandbox risk.
92
+ if (!configPath) return { colors: [], fonts: [], spacing: [] };
93
+ let content;
94
+ try { content = readFileSync(configPath, "utf8"); } catch { return { colors: [], fonts: [], spacing: [] }; }
95
+ const out = { colors: [], fonts: [], spacing: [] };
96
+ // Match: brand: '#abcdef' or brand: "oklch(...)"
97
+ const colorRe = /([a-zA-Z][\w-]*)\s*:\s*['"`]((?:#[0-9a-fA-F]{3,8}|oklch\([^)]+\)|hsl\([^)]+\)|rgb\([^)]+\))[^'"`]*)['"`]/g;
98
+ let m;
99
+ while ((m = colorRe.exec(content)) !== null) {
100
+ out.colors.push({ name: m[1], value: m[2], source: configPath });
101
+ }
102
+ // Match: fontFamily: { serif: ['Fraunces', ...], sans: [...] }
103
+ const fontRe = /fontFamily\s*:\s*\{([\s\S]*?)\}/;
104
+ const fontBlock = fontRe.exec(content);
105
+ if (fontBlock) {
106
+ const entryRe = /([a-zA-Z][\w-]*)\s*:\s*\[([^\]]+)\]/g;
107
+ let f;
108
+ while ((f = entryRe.exec(fontBlock[1])) !== null) {
109
+ const primary = f[2].split(",")[0].trim().replace(/['"`]/g, "");
110
+ out.fonts.push({ name: f[1], primary, source: configPath });
111
+ }
112
+ }
113
+ return out;
114
+ }
115
+
116
+ function extractFontImports() {
117
+ const fonts = new Set();
118
+ // CSS @import url(...)
119
+ for (const f of findStyleFiles()) {
120
+ const content = readFileSync(f, "utf8");
121
+ const importRe = /@import\s+url\(['"]?https?:\/\/fonts\.googleapis\.com\/css2?\?family=([A-Za-z0-9+%]+)/g;
122
+ let m;
123
+ while ((m = importRe.exec(content)) !== null) {
124
+ fonts.add(decodeURIComponent(m[1].replace(/\+/g, " ")));
125
+ }
126
+ }
127
+ // next/font usage in layout files
128
+ for (const layout of ["app/layout.tsx", "app/layout.jsx", "src/app/layout.tsx", "src/app/layout.jsx"]) {
129
+ if (!existsSync(layout)) continue;
130
+ const content = readFileSync(layout, "utf8");
131
+ const nextFontRe = /from\s+['"]next\/font\/google['"][\s\S]{0,400}?import\s*\{\s*([A-Z][a-zA-Z_]*)\s*\}/g;
132
+ // Reverse: import { Fraunces } from 'next/font/google'
133
+ const importRe = /import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]next\/font\/google['"]/g;
134
+ let m;
135
+ while ((m = importRe.exec(content)) !== null) {
136
+ for (const name of m[1].split(",").map((s) => s.trim()).filter(Boolean)) {
137
+ fonts.add(name.replace(/_/g, " "));
138
+ }
139
+ }
140
+ }
141
+ return [...fonts];
142
+ }
143
+
144
+ // ─── DESIGN.md parsing ────────────────────────────────────────────────
145
+
146
+ function readDesignMd(designPath) {
147
+ if (!existsSync(designPath)) {
148
+ console.error(`DESIGN.md not found at ${designPath}`);
149
+ exit(2);
150
+ }
151
+ const content = readFileSync(designPath, "utf8");
152
+ return content;
153
+ }
154
+
155
+ function extractDesignMdTokens(content) {
156
+ const tokens = { vars: [], fonts: [] };
157
+ // CSS-style vars anywhere in DESIGN.md
158
+ const varRe = /--([a-z][a-z0-9-]*)\s*:\s*([^;\n]+);/gi;
159
+ let m;
160
+ while ((m = varRe.exec(content)) !== null) {
161
+ tokens.vars.push({ name: m[1], value: m[2].trim() });
162
+ }
163
+ // Font-family lines
164
+ const fontFamilyRe = /font-family\s*:\s*['"`]?([A-Z][A-Za-z0-9 ]+)['"`]?/g;
165
+ while ((m = fontFamilyRe.exec(content)) !== null) {
166
+ tokens.fonts.push(m[1].trim());
167
+ }
168
+ // Bare font names in §3 (Typography). Match "Font: Fraunces" or "Fraunces" on its own line.
169
+ const namedFontRe = /^\s*(?:Font|font|Family|family)\s*:\s*([A-Z][A-Za-z0-9 ]+)\s*$/gm;
170
+ while ((m = namedFontRe.exec(content)) !== null) {
171
+ tokens.fonts.push(m[1].trim());
172
+ }
173
+ return tokens;
174
+ }
175
+
176
+ // ─── Sync ────────────────────────────────────────────────────────────
177
+
178
+ function cmdSync() {
179
+ const designPath = flag("--design", ".planning/DESIGN.md");
180
+ const write = hasFlag("--write");
181
+ const designContent = readDesignMd(designPath);
182
+ const designTokens = extractDesignMdTokens(designContent);
183
+ const designVarMap = new Map(designTokens.vars.map((v) => [v.name, v.value]));
184
+ const designFonts = new Set(designTokens.fonts.map((f) => f.toLowerCase()));
185
+
186
+ const codeVars = [];
187
+ for (const f of findStyleFiles()) codeVars.push(...extractCssVarsFromFile(f));
188
+ const codeVarMap = new Map();
189
+ for (const v of codeVars) {
190
+ if (!codeVarMap.has(v.name)) codeVarMap.set(v.name, v);
191
+ }
192
+
193
+ const tw = extractTailwindTokens(findTailwindConfig());
194
+ const codeFonts = new Set([
195
+ ...extractFontImports().map((f) => f.toLowerCase()),
196
+ ...tw.fonts.map((f) => f.primary.toLowerCase()),
197
+ ]);
198
+
199
+ const undocumented = []; // in code, not in DESIGN.md
200
+ const orphaned = []; // in DESIGN.md, not in code
201
+ const drifted = []; // both, but value differs
202
+
203
+ for (const [name, v] of codeVarMap) {
204
+ if (!designVarMap.has(name)) {
205
+ undocumented.push({ name, value: v.value, source: v.source });
206
+ } else if (designVarMap.get(name) !== v.value) {
207
+ drifted.push({ name, code: v.value, design: designVarMap.get(name), source: v.source });
208
+ }
209
+ }
210
+ for (const [name, value] of designVarMap) {
211
+ if (!codeVarMap.has(name)) orphaned.push({ name, value });
212
+ }
213
+
214
+ const fontUndocumented = [...codeFonts].filter((f) => !designFonts.has(f));
215
+ const fontOrphaned = [...designFonts].filter((f) => !codeFonts.has(f));
216
+
217
+ // Report
218
+ const report = {
219
+ design_md: designPath,
220
+ style_files: findStyleFiles(),
221
+ tailwind_config: findTailwindConfig(),
222
+ undocumented_vars: undocumented,
223
+ orphaned_vars: orphaned,
224
+ drifted_vars: drifted,
225
+ undocumented_fonts: fontUndocumented,
226
+ orphaned_fonts: fontOrphaned,
227
+ counts: {
228
+ undocumented: undocumented.length + fontUndocumented.length,
229
+ orphaned: orphaned.length + fontOrphaned.length,
230
+ drifted: drifted.length,
231
+ },
232
+ };
233
+
234
+ if (hasFlag("--json")) {
235
+ console.log(JSON.stringify(report, null, 2));
236
+ } else {
237
+ const c = report.counts;
238
+ console.log(`design ↔ code drift report`);
239
+ console.log(` DESIGN.md: ${designPath}`);
240
+ console.log(` Style files: ${report.style_files.join(", ") || "(none found)"}`);
241
+ console.log(` Tailwind config: ${report.tailwind_config || "(none found)"}`);
242
+ console.log(``);
243
+ console.log(` Undocumented in DESIGN.md: ${c.undocumented} (in code, not declared)`);
244
+ for (const v of undocumented.slice(0, 10)) console.log(` --${v.name}: ${v.value} (${v.source})`);
245
+ if (undocumented.length > 10) console.log(` …and ${undocumented.length - 10} more`);
246
+ for (const f of fontUndocumented.slice(0, 5)) console.log(` font: ${f} (loaded but not declared)`);
247
+ console.log(``);
248
+ console.log(` Orphaned in DESIGN.md: ${c.orphaned} (declared, not used in code)`);
249
+ for (const v of orphaned.slice(0, 10)) console.log(` --${v.name}: ${v.value}`);
250
+ if (orphaned.length > 10) console.log(` …and ${orphaned.length - 10} more`);
251
+ for (const f of fontOrphaned.slice(0, 5)) console.log(` font: ${f} (declared but never imported)`);
252
+ console.log(``);
253
+ console.log(` Drifted values: ${c.drifted} (different in code vs DESIGN.md)`);
254
+ for (const d of drifted.slice(0, 10)) {
255
+ console.log(` --${d.name}:`);
256
+ console.log(` code: ${d.code} (${d.source})`);
257
+ console.log(` DESIGN: ${d.design}`);
258
+ }
259
+ if (drifted.length > 10) console.log(` …and ${drifted.length - 10} more`);
260
+ }
261
+
262
+ // Patch
263
+ if (write && (undocumented.length + drifted.length > 0)) {
264
+ let patched = designContent;
265
+ // Append an Updated tokens section with the code-truth values.
266
+ const stamp = new Date().toISOString();
267
+ const section = [
268
+ ``,
269
+ `## §sync — auto-synced from code (${stamp})`,
270
+ ``,
271
+ `<!-- Generated by /qualia-vibe --sync --write. Reflects values actually present in code at sync time. -->`,
272
+ ``,
273
+ "```css",
274
+ `:root {`,
275
+ ...undocumented.map((v) => ` --${v.name}: ${v.value}; /* from ${v.source} */`),
276
+ ...drifted.map((d) => ` --${d.name}: ${d.code}; /* was: ${d.design} */`),
277
+ `}`,
278
+ "```",
279
+ ``,
280
+ ].join("\n");
281
+ patched = patched.trimEnd() + "\n" + section;
282
+ writeFileSync(designPath, patched, "utf8");
283
+ console.log(``);
284
+ console.log(`✓ DESIGN.md patched with ${undocumented.length + drifted.length} token updates`);
285
+ }
286
+
287
+ // Exit code: success if no drift OR --write applied; informational fail otherwise.
288
+ const totalDrift = report.counts.undocumented + report.counts.orphaned + report.counts.drifted;
289
+ if (totalDrift === 0) exit(0);
290
+ if (write) exit(0);
291
+ exit(1);
292
+ }
293
+
294
+ // ─── Variants scaffold ───────────────────────────────────────────────
295
+
296
+ function cmdProposeVariants() {
297
+ const productPath = flag("--product", ".planning/PRODUCT.md");
298
+ const designPath = flag("--design", ".planning/DESIGN.md");
299
+ const count = parseInt(flag("--count", "3"), 10);
300
+ if (!Number.isFinite(count) || count < 2 || count > 5) {
301
+ console.error("--count must be 2..5");
302
+ exit(2);
303
+ }
304
+
305
+ const product = existsSync(productPath) ? readFileSync(productPath, "utf8") : "";
306
+ const design = existsSync(designPath) ? readFileSync(designPath, "utf8") : "";
307
+
308
+ // Emit a structured scaffold the LLM uses to generate variants.
309
+ const scaffold = {
310
+ instruction: `Generate exactly ${count} aesthetic-direction variants for /qualia-vibe --variants. Each variant must be opinionated and concrete — no "modern minimal" hedging. Variants must be meaningfully different from each other AND from the current direction.`,
311
+ context: {
312
+ product_md: product.slice(0, 4000),
313
+ current_direction_lines: design
314
+ .split("\n")
315
+ .filter((l) => /aesthetic\s+direction|direction\s*:/i.test(l))
316
+ .slice(0, 5),
317
+ },
318
+ output_contract: {
319
+ shape: `One variant per block, exactly N=${count} blocks. Per variant: name, direction (1 sentence), color (1 sentence), typography (1 sentence), motion (1 sentence). No prose justification beyond those 4 lines.`,
320
+ },
321
+ rules: [
322
+ "No banned fonts (Inter/Roboto/Arial/Helvetica/system-ui/Space Grotesk/Montserrat/Poppins/Lato/Open Sans).",
323
+ "No purple-blue gradients.",
324
+ "No bounce/elastic easing.",
325
+ `Respect anti-references from PRODUCT.md if present.`,
326
+ "Each variant should be commit-able as-is — concrete enough that /qualia-vibe can apply it without further questions.",
327
+ ],
328
+ };
329
+
330
+ console.log(JSON.stringify(scaffold, null, 2));
331
+ exit(0);
332
+ }
333
+
334
+ // ─── Dispatch ────────────────────────────────────────────────────────
335
+
336
+ switch (CMD) {
337
+ case "sync": cmdSync(); break;
338
+ case "propose-variants": cmdProposeVariants(); break;
339
+ default:
340
+ console.error(`Unknown command: ${CMD}`);
341
+ exit(2);
342
+ }
@@ -297,7 +297,7 @@
297
297
  <div class="header-content">
298
298
  <h1><span>Qualia</span> Framework</h1>
299
299
  <p>Plan, build, verify, ship. The AI-powered workflow for Qualia Solutions.</p>
300
- <div class="version">{{VERSION}} &middot; 28 skills</div>
300
+ <div class="version">{{VERSION}} &middot; 33 skills</div>
301
301
  </div>
302
302
  </div>
303
303
 
@@ -366,6 +366,9 @@
366
366
  <div class="cmd"><span class="cmd-name">/qualia-review</span><span class="cmd-desc">Production audit with scored diagnostics. Runs real commands, scores findings by severity. Trigger on 'review', 'audit', 'code review', 'security check'.</span></div>
367
367
  <div class="cmd"><span class="cmd-name">/qualia-optimize</span><span class="cmd-desc">Deep optimization pass &mdash; reads .planning/ AND codebase to find performance, design, UI, backend, and frontend issues. Spawns parallel specialist agents. Supports --perf, --ui, --backend, --alignment, --fix flags.</span></div>
368
368
  <div class="cmd"><span class="cmd-name">/qualia-test</span><span class="cmd-desc">Generate or run tests for client projects. Trigger on 'write tests', 'add tests', 'test this', 'test coverage'.</span></div>
369
+ <div class="cmd"><span class="cmd-name">/qualia-zoom</span><span class="cmd-desc">Focus on a single file or function with full context (glossary terms, depending callers, ADRs touched). Use when a fresh agent needs surgical context for a tricky area.</span></div>
370
+ <div class="cmd"><span class="cmd-name">/qualia-issues</span><span class="cmd-desc">Break a phase plan into independent vertical-slice GitHub issues with needs-triage label. Externalizes work to the open queue so other sessions or contributors can pull from it.</span></div>
371
+ <div class="cmd"><span class="cmd-name">/qualia-triage</span><span class="cmd-desc">State machine over open GH issues — labels each as needs-triage, needs-info, ready-for-agent, ready-for-human, or wontfix. Pairs with /qualia-issues for the autonomous queue.</span></div>
369
372
  </div>
370
373
  </div>
371
374
 
@@ -376,6 +379,7 @@
376
379
  <div class="commands">
377
380
  <div class="cmd"><span class="cmd-name">/qualia-feature</span><span class="cmd-desc">Auto-scoped single-feature build. Inline for trivia (typo, config), fresh builder spawn for 1-5 file features. Refuses and routes to /qualia-plan for phase-sized work. Flags: --force-spawn, --force-inline.</span></div>
378
381
  <div class="cmd"><span class="cmd-name">/qualia-polish</span><span class="cmd-desc">Design pass, scope-adaptive &mdash; component, route, full app, redesign, critique, quick. Add --loop for the autonomous screenshot &rarr; score &rarr; fix loop.</span></div>
382
+ <div class="cmd"><span class="cmd-name">/qualia-vibe</span><span class="cmd-desc">Fast aesthetic pivot (~3 min) &mdash; swap design tokens (color, type, depth, motion), keep layout untouched. Default proposes ONE direction. Modes: --variants for A/B/C, --extract URL to reverse-engineer DESIGN.md from a reference site, --sync for code &harr; DESIGN.md back-sync.</span></div>
379
383
  </div>
380
384
  </div>
381
385
 
@@ -408,6 +412,7 @@
408
412
  <div class="cmd"><span class="cmd-name">/qualia</span><span class="cmd-desc">Smart router &mdash; reads project state, classifies your situation, tells you the exact next command. Use whenever you're unsure about your next step.</span></div>
409
413
  <div class="cmd"><span class="cmd-name">/qualia-idk</span><span class="cmd-desc">Diagnostic intelligence &mdash; spawns two isolated scans (planning + codebase) in parallel, cross-references against your confusion, explains the situation in plain language with a concrete next step. Use when something feels off or you need to understand what's going on.</span></div>
410
414
  <div class="cmd"><span class="cmd-name">/qualia-help</span><span class="cmd-desc">Open the Qualia Framework reference guide in the browser. A beautiful themed HTML page with all commands, rules, services, and the road.</span></div>
415
+ <div class="cmd"><span class="cmd-name">/qualia-road</span><span class="cmd-desc">Terminal workflow map — Project → Journey → Milestones → Phases → Tasks. Use in headless/SSH sessions or when you want the road in chat instead of the browser.</span></div>
411
416
  </div>
412
417
  </div>
413
418
 
@@ -418,6 +423,8 @@
418
423
  <div class="commands">
419
424
  <div class="cmd"><span class="cmd-name">/qualia-skill-new</span><span class="cmd-desc">Author a new Qualia skill or agent. Generates the SKILL.md, registers it in the right location, and optionally ships to the framework repo.</span></div>
420
425
  <div class="cmd"><span class="cmd-name">/qualia-postmortem</span><span class="cmd-desc">Analyze a verification failure and turn the lesson into a framework improvement.</span></div>
426
+ <div class="cmd"><span class="cmd-name">/qualia-hook-gen</span><span class="cmd-desc">Convert a CLAUDE.md or rules instruction into a deterministic Claude Code pre-tool-use hook. Lets you shrink your instruction budget instead of just hearing the advice.</span></div>
427
+ <div class="cmd"><span class="cmd-name">/zoho-workflow</span><span class="cmd-desc">Internal Qualia Solutions ops — Zoho Invoice and Mail integration via ERP-first routing.</span></div>
421
428
  </div>
422
429
  </div>
423
430
  </section>
@@ -541,7 +548,7 @@
541
548
  <div class="footer">
542
549
  <strong>Welcome to the future with Qualia.</strong><br>
543
550
  Qualia Solutions &mdash; Nicosia, Cyprus
544
- <span class="footer-version">qualia-framework {{VERSION}} &middot; 28 skills</span>
551
+ <span class="footer-version">qualia-framework {{VERSION}} &middot; 33 skills</span>
545
552
  </div>
546
553
 
547
554
  </body>
package/tests/bin.test.sh CHANGED
@@ -1225,11 +1225,11 @@ else
1225
1225
  fail_case "qualia-road missing /qualia-polish --loop reference"
1226
1226
  fi
1227
1227
 
1228
- # 108. package.json version is 5.x (5.1+ accepted; v5.1 / v5.2 share the v5 line)
1229
- if grep -qE '"5\.([1-9]|[1-9][0-9])\.' "$FRAMEWORK_DIR/package.json"; then
1230
- pass "package.json version is 5.x"
1228
+ # 108. package.json version is v5.1+ or v6+ (5.1+ accepted; v6 acceptable from v6.0.0)
1229
+ if grep -qE '"(5\.([1-9]|[1-9][0-9])|[6-9]|[1-9][0-9])\.' "$FRAMEWORK_DIR/package.json"; then
1230
+ pass "package.json version is v5.1+ or v6+"
1231
1231
  else
1232
- fail_case "package.json version not 5.x"
1232
+ fail_case "package.json version not v5.1+ or v6+"
1233
1233
  fi
1234
1234
 
1235
1235
  # 109. loop.mjs installs (orchestrator)
@@ -1453,12 +1453,12 @@ else
1453
1453
  fail_case "qualia-ui CLI broke"
1454
1454
  fi
1455
1455
 
1456
- # 128. package.json bumped to 5.x (5.1+ accepted; 5.2 is the v5.2 release)
1456
+ # 128. package.json bumped to v5.1+ or v6+
1457
1457
  PKG_V=$($NODE -e 'console.log(require("'"$FRAMEWORK_DIR"'/package.json").version)')
1458
- if echo "$PKG_V" | grep -qE "^5\.([1-9]|[1-9][0-9])\."; then
1459
- pass "package.json version bumped to 5.x ($PKG_V)"
1458
+ if echo "$PKG_V" | grep -qE "^(5\.([1-9]|[1-9][0-9])|[6-9]|[1-9][0-9])\."; then
1459
+ pass "package.json version bumped ($PKG_V)"
1460
1460
  else
1461
- fail_case "package.json version not 5.x" "got=$PKG_V"
1461
+ fail_case "package.json version not v5.1+ or v6+" "got=$PKG_V"
1462
1462
  fi
1463
1463
 
1464
1464
  echo ""
@@ -1605,12 +1605,12 @@ else
1605
1605
  fail_case "qualia-optimize REFERENCE.md missing parallel-interface template"
1606
1606
  fi
1607
1607
 
1608
- # 143. package.json version is 5.x (5.1+ accepted; v5.3 is the v5.3 release)
1608
+ # 143. package.json version is v5.1+ or v6+
1609
1609
  PKG_V=$($NODE -e 'console.log(require("'"$FRAMEWORK_DIR"'/package.json").version)')
1610
- if echo "$PKG_V" | grep -qE "^5\.([1-9]|[1-9][0-9])\."; then
1611
- pass "package.json version is 5.x ($PKG_V) — v5.3 accepted"
1610
+ if echo "$PKG_V" | grep -qE "^(5\.([1-9]|[1-9][0-9])|[6-9]|[1-9][0-9])\."; then
1611
+ pass "package.json version is shipping range ($PKG_V)"
1612
1612
  else
1613
- fail_case "package.json version not 5.x" "got=$PKG_V"
1613
+ fail_case "package.json version not v5.1+ or v6+" "got=$PKG_V"
1614
1614
  fi
1615
1615
 
1616
1616
  echo ""
@@ -42,7 +42,7 @@ EXCLUDE_REGEX='/docs/reviews/|/docs/research/|/docs/playwright-loop-pilot-result
42
42
  # When a `/qualia-foo` ref appears AFTER one of these context tokens on the same line,
43
43
  # it's a migration-explainer ("Replaces /qualia-quick" / "deprecated in v5.7"), not
44
44
  # a live command reference. Treat it as exempt.
45
- MIGRATION_CONTEXT_REGEX='Replaces|Removed|removed in|consolidated|deprecated|renamed|former|previously|was the|now the|now\s+`?/qualia|absorbed|superseded|legacy|migrated|after\s+`?/qualia.*-(quick|task|prd|design|polish-loop)'
45
+ MIGRATION_CONTEXT_REGEX='Replaces|Removed|removed in|consolidated|deprecated|renamed|former|previously|was the|now the|now\s+`?/qualia|absorbed|superseded|legacy|migrated|dead|after\s+`?/qualia.*-(quick|task|prd|design|polish-loop)'
46
46
 
47
47
  ACTIVE_DIRS=(
48
48
  "$FRAMEWORK_ROOT/rules"
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env bash
2
+ # tests/run-all.sh — fail-collect test orchestrator.
3
+ # Runs every suite even if earlier ones fail, then reports which suites failed.
4
+ # Exits non-zero if any suite failed.
5
+
6
+ set -u
7
+
8
+ DIR="$(cd "$(dirname "$0")" && pwd)"
9
+
10
+ SUITES=(
11
+ "statusline"
12
+ "state"
13
+ "hooks"
14
+ "bin"
15
+ "lib"
16
+ "skills"
17
+ "refs"
18
+ "slop-detect"
19
+ )
20
+
21
+ FAILED=()
22
+
23
+ for suite in "${SUITES[@]}"; do
24
+ file="$DIR/$suite.test.sh"
25
+ if [ ! -f "$file" ]; then
26
+ echo "MISSING: $file"
27
+ FAILED+=("$suite (missing file)")
28
+ continue
29
+ fi
30
+ if bash "$file"; then
31
+ :
32
+ else
33
+ FAILED+=("$suite")
34
+ fi
35
+ done
36
+
37
+ echo ""
38
+ echo "===================================================="
39
+ if [ ${#FAILED[@]} -eq 0 ]; then
40
+ echo "All ${#SUITES[@]} suites passed."
41
+ exit 0
42
+ else
43
+ echo "${#FAILED[@]} of ${#SUITES[@]} suites FAILED:"
44
+ for s in "${FAILED[@]}"; do
45
+ echo " - $s"
46
+ done
47
+ exit 1
48
+ fi
@@ -86,11 +86,13 @@ export default function Page() {
86
86
  return <p>Welcome — to our amazing platform</p>;
87
87
  }
88
88
  EOF
89
- OUT=$($NODE "$SLOP_DETECT" "$TMP2/emdash.tsx" 2>&1 || true)
90
- if echo "$OUT" | grep -qiE "em.?dash|—"; then
89
+ EXIT_CODE=0
90
+ OUT=$($NODE "$SLOP_DETECT" "$TMP2/emdash.tsx" 2>&1) || EXIT_CODE=$?
91
+ # Em-dash is HIGH (non-blocking) so exit MUST be 0; finding MUST be in output.
92
+ if [ "$EXIT_CODE" = "0" ] && echo "$OUT" | grep -qiE "em.?dash|—"; then
91
93
  pass "reports em-dash finding (HIGH severity, non-blocking)"
92
94
  else
93
- fail_case "em-dash detection" "no em-dash mention in output: $(echo "$OUT" | head -c 120)"
95
+ fail_case "em-dash detection" "exit=$EXIT_CODE, output: $(echo "$OUT" | head -c 120)"
94
96
  fi
95
97
 
96
98
  # ── Banned-font detection ─────────────────────────────────────────────
@@ -138,8 +140,12 @@ fi
138
140
  # ── --json flag produces JSON output ─────────────────────────────────
139
141
  TMP5=$(mktmp)
140
142
  cp "$TMP3/font.css" "$TMP5/font.css"
141
- JSON_OUT=$($NODE "$SLOP_DETECT" --json "$TMP5/font.css" 2>/dev/null || true)
142
- if echo "$JSON_OUT" | head -1 | grep -qE "^[\{\[]"; then
143
+ # --json may exit 1 (banned font is CRITICAL); we only assert output shape.
144
+ EXIT_CODE=0
145
+ JSON_OUT=$($NODE "$SLOP_DETECT" --json "$TMP5/font.css" 2>/dev/null) || EXIT_CODE=$?
146
+ if [ "$EXIT_CODE" != "0" ] && [ "$EXIT_CODE" != "1" ]; then
147
+ fail_case "--json output" "unexpected exit=$EXIT_CODE (only 0/1 acceptable for CRITICAL)"
148
+ elif echo "$JSON_OUT" | head -1 | grep -qE "^[\{\[]"; then
143
149
  pass "--json flag produces JSON-shaped output"
144
150
  else
145
151
  fail_case "--json output" "first line is not JSON-shaped: '$(echo "$JSON_OUT" | head -c 80)'"