qualia-framework 6.7.1 → 6.8.1

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/bin/install.js CHANGED
@@ -210,13 +210,22 @@ function ensureCodexStatusLineConfig(existing) {
210
210
  return `${next.slice(0, tuiMatch.index)}${tuiBlock}${next.slice(tuiMatch.index + tuiMatch[0].length)}`;
211
211
  }
212
212
 
213
+ // v6.8.1: .bak files go into a backups/ subdir next to the target instead of
214
+ // littering the target's directory (a dozen settings.json.bak.* files were
215
+ // accumulating in ~/.claude root across reinstalls).
216
+ function bakPath(dest) {
217
+ const dir = path.join(path.dirname(dest), "backups");
218
+ try { fs.mkdirSync(dir, { recursive: true }); } catch {}
219
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
220
+ return path.join(dir, `${path.basename(dest)}.bak.${ts}`);
221
+ }
222
+
213
223
  function backupIfDifferent(dest, nextContent, label) {
214
224
  if (!fs.existsSync(dest)) return false;
215
225
  try {
216
226
  const existing = fs.readFileSync(dest, "utf8");
217
227
  if (existing === nextContent) return false;
218
- const ts = new Date().toISOString().replace(/[:.]/g, "-");
219
- const bak = `${dest}.bak.${ts}`;
228
+ const bak = bakPath(dest);
220
229
  fs.copyFileSync(dest, bak);
221
230
  ok(`Backed up existing ${label} -> ${path.basename(bak)}`);
222
231
  return true;
@@ -343,7 +352,10 @@ function cleanupLegacyV26() {
343
352
  // ─── Branded Header ─────────────────────────────────────
344
353
  const BOLD = "\x1b[1m";
345
354
  const TEAL_GLOW = "\x1b[38;2;0;170;175m";
346
- const PKG_VERSION = require("../package.json").version;
355
+ // Defensive: the installed copy may lack a root package.json (Wave 1.2 writes one
356
+ // post-install, but a partially-stripped install must not MODULE_NOT_FOUND here).
357
+ let PKG_VERSION = "0.0.0";
358
+ try { PKG_VERSION = require("../package.json").version; } catch {}
347
359
  const RULE = "━".repeat(48);
348
360
 
349
361
  function printHeader() {
@@ -672,7 +684,17 @@ async function main() {
672
684
  // doctor remains, but only strips the OLD legacy command (which our v2
673
685
  // hook does not match — different content, same filename is fine because
674
686
  // install always overwrites).
675
- const DEPRECATED_HOOKS = ["block-env-edit.js"];
687
+ const DEPRECATED_HOOKS = [
688
+ "block-env-edit.js",
689
+ // Retired "brain" hooks from an abandoned experiment — never shipped in
690
+ // hooksSource, so the orphan-purge below also catches them; listed here as
691
+ // belt-and-suspenders for installs predating the orphan pass.
692
+ "brain-pre-compact.js",
693
+ "brain-session-end.js",
694
+ "brain-session-start.js",
695
+ // v6.8.1: the UserPromptSubmit half of the brain experiment was missed.
696
+ "brain-inject.js",
697
+ ];
676
698
  for (const f of DEPRECATED_HOOKS) {
677
699
  const p = path.join(hooksDest, f);
678
700
  try { if (fs.existsSync(p)) fs.unlinkSync(p); } catch {}
@@ -688,6 +710,16 @@ async function main() {
688
710
  warn(`${file} — ${e.message}`);
689
711
  }
690
712
  }
713
+ // Orphan purge (idempotency): remove any .js hook in the dest that the
714
+ // framework no longer ships, so retired hooks don't keep firing after upgrade.
715
+ try {
716
+ const srcHooks = new Set(fs.readdirSync(hooksSource).filter((f) => f.endsWith(".js")));
717
+ for (const f of fs.readdirSync(hooksDest).filter((f) => f.endsWith(".js"))) {
718
+ if (!srcHooks.has(f)) {
719
+ try { fs.unlinkSync(path.join(hooksDest, f)); warn(`pruned orphan hook ${f}`); } catch {}
720
+ }
721
+ }
722
+ } catch {}
691
723
 
692
724
  // ─── Templates (recursive — supports nested projects/ and research-project/) ─
693
725
  printSection("Templates");
@@ -761,6 +793,16 @@ async function main() {
761
793
  warn(`${file} — ${e.message}`);
762
794
  }
763
795
  }
796
+ // Canonical copy: qualia-scope + constitution read references/archetypes/*.md
797
+ // from CLAUDE_DIR/references (not qualia-references). Copy the whole tree
798
+ // recursively so nested dirs like archetypes/ land at the canonical path.
799
+ try {
800
+ const refDestCanonical = path.join(CLAUDE_DIR, "references");
801
+ copyTreeTransform(refDir, refDestCanonical, claudeText);
802
+ ok("references/ (canonical tree incl. archetypes/)");
803
+ } catch (e) {
804
+ warn(`references/ canonical — ${e.message}`);
805
+ }
764
806
  } else {
765
807
  log(`${DIM}(no references/ in framework — skipping)${RESET}`);
766
808
  }
@@ -789,9 +831,8 @@ async function main() {
789
831
  if (fs.existsSync(claudeDest)) {
790
832
  const existing = fs.readFileSync(claudeDest, "utf8");
791
833
  if (existing !== claudeMd) {
792
- const ts = new Date().toISOString().replace(/[:.]/g, "-");
793
- const bak = `${claudeDest}.bak.${ts}`;
794
- try { fs.copyFileSync(claudeDest, bak); ok(`Backed up existing CLAUDE.md → ${path.basename(bak)}`); } catch {}
834
+ const bak = bakPath(claudeDest);
835
+ try { fs.copyFileSync(claudeDest, bak); ok(`Backed up existing CLAUDE.md → backups/${path.basename(bak)}`); } catch {}
795
836
  }
796
837
  }
797
838
  fs.writeFileSync(claudeDest, claudeText(claudeMd), "utf8");
@@ -811,6 +852,21 @@ async function main() {
811
852
  try { fs.chmodSync(out, 0o755); } catch {}
812
853
  ok(script.label);
813
854
  }
855
+ // v6.8.1: purge retired bin scripts (same belt-and-suspenders as
856
+ // DEPRECATED_HOOKS — bin/ never had an orphan pass, so the brain
857
+ // experiment's indexer survived reinstalls).
858
+ const DEPRECATED_BIN = ["build-brain-index.js"];
859
+ for (const f of DEPRECATED_BIN) {
860
+ const p = path.join(binDest, f);
861
+ try { if (fs.existsSync(p)) fs.unlinkSync(p); } catch {}
862
+ }
863
+ // Write a minimal root package.json so the installed CLI's `require("../package.json")`
864
+ // resolves post-install (bin/ lives at CLAUDE_DIR/bin, so the parent is CLAUDE_DIR).
865
+ fs.writeFileSync(
866
+ path.join(CLAUDE_DIR, "package.json"),
867
+ JSON.stringify({ name: "qualia-framework-install", version: PKG_VERSION, private: true }, null, 2) + "\n",
868
+ );
869
+ ok("package.json (root version marker)");
814
870
  } catch (e) {
815
871
  warn(`scripts — ${e.message}`);
816
872
  }
@@ -827,6 +883,16 @@ async function main() {
827
883
  warn(`guide.md — ${e.message}`);
828
884
  }
829
885
 
886
+ // ─── Companion docs (read by skills/agents + linked from guide.md) ─────
887
+ for (const doc of ["SOUL.md", "FLAGS.md", "TROUBLESHOOTING.md", "CHANGELOG.md"]) {
888
+ try {
889
+ copyTextTransform(path.join(FRAMEWORK_DIR, doc), path.join(CLAUDE_DIR, doc), claudeText);
890
+ ok(doc);
891
+ } catch (e) {
892
+ warn(`${doc} — ${e.message}`);
893
+ }
894
+ }
895
+
830
896
  // ─── Knowledge directory ─────────────────────────────────
831
897
  printSection("Knowledge Base");
832
898
  const knowledgeDir = path.join(CLAUDE_DIR, "knowledge");
@@ -993,6 +1059,9 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
993
1059
  } catch {}
994
1060
  }
995
1061
 
1062
+ // Schema marker for editor validation / autocomplete of settings.json.
1063
+ settings["$schema"] = "https://json.schemastore.org/claude-code-settings.json";
1064
+
996
1065
  // Env
997
1066
  if (!settings.env) settings.env = {};
998
1067
  Object.assign(settings.env, {
@@ -1008,7 +1077,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
1008
1077
  // context exists in the current session; verifier and plan-checker still
1009
1078
  // use blank-context spawns to avoid the "kid grading their own homework"
1010
1079
  // failure mode.
1011
- CLAUDE_AGENT_FORK_ENABLED: "1",
1080
+ CLAUDE_CODE_FORK_SUBAGENT: "1",
1012
1081
  });
1013
1082
 
1014
1083
  // Status line
@@ -1091,7 +1160,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
1091
1160
  { type: "command", command: nodeCmd("fawzi-approval-guard.js"), timeout: 5 },
1092
1161
  { type: "command", if: "Bash(git push*)", command: nodeCmd("branch-guard.js"), timeout: 5, statusMessage: "⬢ Checking branch permissions..." },
1093
1162
  { type: "command", if: "Bash(git push*)", command: nodeCmd("pre-push.js"), timeout: 15, statusMessage: "⬢ Syncing tracking..." },
1094
- { type: "command", if: "Bash(vercel --prod*)", command: nodeCmd("pre-deploy-gate.js"), timeout: 180, statusMessage: "⬢ Running quality gates..." },
1163
+ { type: "command", if: "Bash(vercel --prod*)", command: nodeCmd("pre-deploy-gate.js"), timeout: 600, statusMessage: "⬢ Running quality gates..." },
1095
1164
  // v5.0 hooks — insights-driven friction prevention
1096
1165
  { type: "command", if: "Bash(vercel --prod*)|Bash(vercel deploy*)", command: nodeCmd("vercel-account-guard.js"), timeout: 8, statusMessage: "⬢ Verifying Vercel account..." },
1097
1166
  { type: "command", if: "Bash(vercel env*)", command: nodeCmd("env-empty-guard.js"), timeout: 5, statusMessage: "⬢ Checking env value..." },
@@ -1153,10 +1222,50 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
1153
1222
  }
1154
1223
  }
1155
1224
 
1225
+ // v6.8.1: sweep ALL hook events for retired Qualia hooks and dead node
1226
+ // paths. The merge above only visits events present in qualiaHooks, so
1227
+ // entries under other events (e.g. the retired brain-inject.js under
1228
+ // UserPromptSubmit) survived every reinstall — firing a failing node
1229
+ // process on each user prompt. Runs after the hooks/ install, so an
1230
+ // existsSync miss means the file is truly gone, not not-yet-copied.
1231
+ const DEPRECATED_HOOK_CMDS = [
1232
+ "brain-inject.js", "build-brain-index.js", "block-env-edit.js",
1233
+ "brain-pre-compact.js", "brain-session-end.js", "brain-session-start.js",
1234
+ ];
1235
+ const isDeadHookCmd = (cmd) => {
1236
+ if (typeof cmd !== "string") return false;
1237
+ if (DEPRECATED_HOOK_CMDS.some((f) => cmd.includes(f))) return true;
1238
+ const m = cmd.match(/^node "([^"]+)"$/);
1239
+ if (m && m[1].startsWith(CLAUDE_DIR + path.sep) && !fs.existsSync(m[1])) return true;
1240
+ return false;
1241
+ };
1242
+ for (const event of Object.keys(settings.hooks)) {
1243
+ const blocks = Array.isArray(settings.hooks[event]) ? settings.hooks[event] : [];
1244
+ const cleaned = [];
1245
+ for (const block of blocks) {
1246
+ if (!block || !Array.isArray(block.hooks)) continue;
1247
+ const kept = block.hooks.filter((h) => !isDeadHookCmd(h && h.command));
1248
+ if (kept.length > 0) cleaned.push({ ...block, hooks: kept });
1249
+ }
1250
+ if (cleaned.length > 0) settings.hooks[event] = cleaned;
1251
+ else delete settings.hooks[event];
1252
+ }
1253
+
1156
1254
  // Permissions stay permissive; Qualia policy enforcement happens in hooks so
1157
- // OWNER overrides and EMPLOYEE blocks can share one source of truth.
1255
+ // OWNER overrides and EMPLOYEE blocks can share one source of truth. We still
1256
+ // seed a scoped baseline allow-list (union-merged, never clobbering user
1257
+ // entries) so common safe tooling has an explicit allow surface rather than
1258
+ // an empty array that matches nothing.
1158
1259
  if (!settings.permissions) settings.permissions = {};
1159
- if (!settings.permissions.allow) settings.permissions.allow = [];
1260
+ const QUALIA_DEFAULT_ALLOW = [
1261
+ "Bash(git *)", "Bash(gh *)", "Bash(npx supabase *)",
1262
+ "Bash(vercel *)", "Bash(npx tsc*)", "Bash(npm run *)",
1263
+ "Read(*)", "Grep(*)", "Glob(*)",
1264
+ ];
1265
+ if (!Array.isArray(settings.permissions.allow)) settings.permissions.allow = [];
1266
+ for (const a of QUALIA_DEFAULT_ALLOW) {
1267
+ if (!settings.permissions.allow.includes(a)) settings.permissions.allow.push(a);
1268
+ }
1160
1269
  if (!settings.permissions.deny) settings.permissions.deny = [];
1161
1270
 
1162
1271
  // ─── Optional: next-devtools MCP ─────────────────────────
@@ -1176,9 +1285,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
1176
1285
  // configs / custom permissions. Atomic write (tmp + rename) avoids partial
1177
1286
  // writes; the .bak file is the recovery point if the merger ever misbehaves.
1178
1287
  if (fs.existsSync(settingsPath)) {
1179
- const ts = new Date().toISOString().replace(/[:.]/g, "-");
1180
- const bak = `${settingsPath}.bak.${ts}`;
1181
- try { fs.copyFileSync(settingsPath, bak); } catch {}
1288
+ try { fs.copyFileSync(settingsPath, bakPath(settingsPath)); } catch {}
1182
1289
  }
1183
1290
  const settingsTmp = `${settingsPath}.tmp.${process.pid}`;
1184
1291
  fs.writeFileSync(settingsTmp, JSON.stringify(settings, null, 2));
@@ -1542,7 +1649,7 @@ async function installCodex(member, target) {
1542
1649
  { type: "command", command: nodeCmd("fawzi-approval-guard.js"), timeout: 5 },
1543
1650
  { type: "command", command: nodeCmd("branch-guard.js"), timeout: 5 },
1544
1651
  { type: "command", command: nodeCmd("pre-push.js"), timeout: 15 },
1545
- { type: "command", command: nodeCmd("pre-deploy-gate.js"), timeout: 180 },
1652
+ { type: "command", command: nodeCmd("pre-deploy-gate.js"), timeout: 600 },
1546
1653
  { type: "command", command: nodeCmd("vercel-account-guard.js"), timeout: 8 },
1547
1654
  { type: "command", command: nodeCmd("env-empty-guard.js"), timeout: 5 },
1548
1655
  { type: "command", command: nodeCmd("supabase-destructive-guard.js"), timeout: 5 },
@@ -18,6 +18,7 @@ const RUNTIME_BIN_SCRIPTS = [
18
18
  { file: "erp-retry.js", label: "erp-retry.js (ERP report retry queue — drained by session-start hook and erp-flush CLI)" },
19
19
  { file: "work-packet.js", label: "work-packet.js (ERP mission/work packet pull + local reader)" },
20
20
  { file: "report-payload.js", label: "report-payload.js (Framework -> ERP report payload builder)" },
21
+ { file: "auto-report.js", label: "auto-report.js (B1 ship-time auto-report)" },
21
22
  { file: "project-snapshot.js", label: "project-snapshot.js (ERP/admin project progress snapshot)" },
22
23
  { file: "trust-score.js", label: "trust-score.js (harness health scoring)" },
23
24
  { file: "harness-eval.js", label: "harness-eval.js (project eval scoring + evidence artifact)" },
@@ -192,7 +192,7 @@ function categoryScore(findings) {
192
192
  return { weighted_sum: ws, score: Math.max(1, 5 - Math.floor(ws / 8)), counts };
193
193
  }
194
194
 
195
- function renderMarkdown({ findings, paths, score }) {
195
+ function renderMarkdown({ findings, paths, score, skipped = [] }) {
196
196
  const lines = [];
197
197
  lines.push(`# Qualia security scan — ${new Date().toISOString()}`);
198
198
  lines.push("");
@@ -202,10 +202,16 @@ function renderMarkdown({ findings, paths, score }) {
202
202
  lines.push(`**Score:** ${score.score} / 5 (weighted_sum=${score.weighted_sum})`);
203
203
  lines.push("");
204
204
 
205
- if (findings.length === 0) {
205
+ if (skipped.length > 0) {
206
+ lines.push(`⚠️ **${skipped.length} file(s) could not be scanned** — result is NOT clean (score degraded, non-zero exit):`);
207
+ for (const s of skipped) lines.push(`- \`${s.file}\` — ${s.error}`);
208
+ lines.push("");
209
+ }
210
+
211
+ if (findings.length === 0 && skipped.length === 0) {
206
212
  lines.push("✅ Clean.");
207
213
  lines.push("");
208
- } else {
214
+ } else if (findings.length > 0) {
209
215
  findings.sort((a, b) => severityOrder(a.severity) - severityOrder(b.severity));
210
216
  for (const f of findings) {
211
217
  lines.push(`### [${f.severity}] ${f.name}`);
@@ -346,6 +352,7 @@ function main() {
346
352
  const paths = args.paths || defaultPaths();
347
353
  const findings = [];
348
354
  const scanned = [];
355
+ const skipped = [];
349
356
 
350
357
  for (const p of paths) {
351
358
  try {
@@ -356,14 +363,21 @@ function main() {
356
363
  const content = fs.readFileSync(p, "utf8");
357
364
  scanned.push(p);
358
365
  findings.push(...scanContent(p, content));
359
- } catch {}
366
+ } catch (e) { skipped.push({ file: p, error: e.message }); }
360
367
  }
361
368
 
362
369
  const score = categoryScore(findings);
370
+ // An unreadable file must not silently pass as clean. Degrade the score
371
+ // (min 1) and force a non-clean exit when any target could not be scanned.
372
+ if (skipped.length > 0) score.score = Math.max(1, score.score - 1);
373
+
374
+ // A scan that couldn't read every target must not exit clean — surface at
375
+ // least HIGH (1) so an unreadable file can't slip through a CI gate as 0.
376
+ const exitCode = skipped.length > 0 ? Math.max(1, exitCodeFor(findings)) : exitCodeFor(findings);
363
377
 
364
378
  if (args.json) {
365
- process.stdout.write(JSON.stringify({ findings, scanned, score }, null, 2) + "\n");
366
- process.exit(exitCodeFor(findings));
379
+ process.stdout.write(JSON.stringify({ findings, scanned, skipped, score }, null, 2) + "\n");
380
+ process.exit(exitCode);
367
381
  }
368
382
 
369
383
  // --deep emits a prompt pack for the /qualia-secure skill to spawn agents.
@@ -373,17 +387,17 @@ function main() {
373
387
  try { fs.mkdirSync(planningDir, { recursive: true }); } catch {}
374
388
  const staticPath = path.join(planningDir, "security-scan.md");
375
389
  const promptPath = path.join(planningDir, "security-deep-prompt.md");
376
- fs.writeFileSync(staticPath, renderMarkdown({ findings, paths: scanned, score }));
390
+ fs.writeFileSync(staticPath, renderMarkdown({ findings, paths: scanned, score, skipped }));
377
391
  fs.writeFileSync(promptPath, renderDeepPromptPack({ findings, scanned, score }));
378
392
  console.log(`Wrote ${staticPath}`);
379
393
  console.log(`Wrote ${promptPath}`);
380
394
  console.log("");
381
395
  console.log("Now run `/qualia-secure` in a Claude session — it will read the prompt pack");
382
396
  console.log("and spawn the red/blue/auditor agents in parallel.");
383
- process.exit(exitCodeFor(findings));
397
+ process.exit(exitCode);
384
398
  }
385
399
 
386
- const md = renderMarkdown({ findings, paths: scanned, score });
400
+ const md = renderMarkdown({ findings, paths: scanned, score, skipped });
387
401
  if (args.write) {
388
402
  const outDefault = path.join(process.cwd(), ".planning", "security-scan.md");
389
403
  const out = args.writePath || outDefault;
@@ -395,7 +409,7 @@ function main() {
395
409
  process.stdout.write(md);
396
410
  }
397
411
 
398
- process.exit(exitCodeFor(findings));
412
+ process.exit(exitCode);
399
413
  }
400
414
 
401
415
  module.exports = { main, scanContent, defaultPaths, categoryScore, renderDeepPromptPack, SECRET_PATTERNS, CONFIG_SMELLS };
@@ -4,6 +4,7 @@
4
4
  const fs = require("fs");
5
5
  const path = require("path");
6
6
  const os = require("os");
7
+ const { spawnSync } = require("child_process");
7
8
  const pc = require("./plan-contract.js");
8
9
  const ledger = require("./state-ledger.js");
9
10
  const { binFiles } = require("./runtime-manifest.js");
@@ -16,6 +17,12 @@ const HOMES = [
16
17
 
17
18
  const REQUIRED_BIN = binFiles();
18
19
 
20
+ // Spine scripts: the load-bearing executables that, if they fail to even parse,
21
+ // render the install dead. binFiles() does not include these, so probe them
22
+ // explicitly — existence AND Node loadability (`--check`). A syntax-broken
23
+ // cli.js (the C1 regression) must force overall status off PASS.
24
+ const SPINE_SCRIPTS = ["cli.js", "install.js", "state.js"];
25
+
19
26
  const REQUIRED_HOOKS = [
20
27
  "session-start.js", "auto-update.js", "branch-guard.js", "pre-push.js",
21
28
  "pre-deploy-gate.js", "migration-guard.js", "git-guardrails.js",
@@ -54,10 +61,27 @@ function inspectInstall(homes) {
54
61
  return { status: "fail", score: 0, issues: ["no Qualia install config found"], targets: [] };
55
62
  }
56
63
  const issues = [];
64
+ let spineBroken = false;
57
65
  for (const home of homes) {
58
66
  for (const f of REQUIRED_BIN) {
59
67
  if (!exists(path.join(home.dir, "bin", f))) issues.push(`${home.name}: missing bin/${f}`);
60
68
  }
69
+ // Spine probe: Node loadability of spine scripts that ARE installed.
70
+ // cli.js / install.js run via npx / the installer and are not part of the
71
+ // runtime manifest, so their ABSENCE is not a failure (state.js absence is
72
+ // already reported via REQUIRED_BIN). But any spine script that IS present
73
+ // must pass `--check` — a syntax-broken installed spine forces status off
74
+ // PASS (this is the check that catches a dead cli.js).
75
+ for (const f of SPINE_SCRIPTS) {
76
+ const fp = path.join(home.dir, "bin", f);
77
+ if (!exists(fp)) continue;
78
+ const r = spawnSync(process.execPath, ["--check", fp], { encoding: "utf8" });
79
+ if (r.status !== 0) {
80
+ const detail = (r.stderr || r.error?.message || "").split("\n")[0].trim();
81
+ issues.push(`${home.name}: spine bin/${f} fails --check${detail ? ` (${detail})` : ""}`);
82
+ spineBroken = true;
83
+ }
84
+ }
61
85
  if (home.name === "Claude") {
62
86
  if (!exists(path.join(home.dir, "CLAUDE.md"))) issues.push("Claude: missing CLAUDE.md");
63
87
  if (!exists(path.join(home.dir, "settings.json"))) issues.push("Claude: missing settings.json");
@@ -67,6 +91,16 @@ function inspectInstall(homes) {
67
91
  if (!exists(path.join(home.dir, "config.toml"))) issues.push("Codex: missing config.toml");
68
92
  }
69
93
  }
94
+ // A broken spine is a hard failure: the install cannot run. Force fail status
95
+ // (drives overall status to FAIL) and zero the score regardless of other issues.
96
+ if (spineBroken) {
97
+ return {
98
+ status: "fail",
99
+ score: 0,
100
+ issues,
101
+ targets: homes.map((h) => h.name),
102
+ };
103
+ }
70
104
  return {
71
105
  status: issues.length ? "degraded" : "pass",
72
106
  score: issues.length ? Math.max(6, 20 - issues.length * 2) : 20,
@@ -161,12 +161,12 @@ if (realCreateTables.length > 0 && !/ENABLE\s+ROW\s+LEVEL\s+SECURITY/i.test(scan
161
161
  }
162
162
 
163
163
  if (errors.length > 0) {
164
- console.log("⬢ Migration guard — dangerous patterns found:");
164
+ console.error("⬢ Migration guard — dangerous patterns found:");
165
165
  for (const e of errors) {
166
- console.log(` ✗ ${e}`);
166
+ console.error(` ✗ ${e}`);
167
167
  }
168
- console.log("");
169
- console.log("Fix these before proceeding. If intentional, ask Fawzi to approve.");
168
+ console.error("");
169
+ console.error("Fix these before proceeding. If intentional, ask Fawzi to approve.");
170
170
  _trace("migration-guard", "block", { errors });
171
171
  process.exit(2);
172
172
  }
@@ -73,7 +73,7 @@ function runGate(label, cmd, args, { required = true } = {}) {
73
73
  const r = spawnSync(cmd, args, {
74
74
  stdio: ["ignore", "pipe", "pipe"],
75
75
  encoding: "utf8",
76
- timeout: 180000,
76
+ timeout: 120000,
77
77
  shell: process.platform === "win32",
78
78
  });
79
79
  if (r.status === 0) {
@@ -170,7 +170,10 @@ function enforceShipPolicy() {
170
170
 
171
171
  // If this is not a Qualia-managed project, keep the legacy behavior: run the
172
172
  // quality/security gates but do not invent state.
173
- if (!state || !status) return;
173
+ if (!state || !status) {
174
+ console.error("⬢ pre-deploy-gate: no ship-state (brownfield) — skipping ship-policy gate, quality gates still run");
175
+ return;
176
+ }
174
177
 
175
178
  const shippable = status === "polished" || (status === "verified" && verification === "pass");
176
179
  if (!shippable && !force) {
@@ -281,9 +284,16 @@ if (fs.existsSync("tsconfig.json")) {
281
284
  }
282
285
 
283
286
  // Lint (with QUALIA_SKIP_LINT=1 escape — for the documented "lint blocks
284
- // ship" friction when the lint failures are pre-existing or auto-fixer-broken)
287
+ // ship" friction when the lint failures are pre-existing or auto-fixer-broken).
288
+ // The skip is OWNER-only, mirroring the QUALIA_SHIP_FORCE role-gate above.
285
289
  if (hasScript("lint")) {
286
- if (process.env.QUALIA_SKIP_LINT === "1") {
290
+ const skipLint = process.env.QUALIA_SKIP_LINT === "1";
291
+ const lintRole = String(readConfig().role || "").toUpperCase();
292
+ if (skipLint && lintRole !== "OWNER") {
293
+ const lintState = readState();
294
+ blockDeploy("QUALIA_SKIP_LINT is OWNER-only.", (lintState && lintState.next_command) || "/qualia");
295
+ }
296
+ if (skipLint) {
287
297
  console.log(" ⚠ Lint skipped (QUALIA_SKIP_LINT=1)");
288
298
  _trace("pre-deploy-gate", "skip-lint", { reason: "QUALIA_SKIP_LINT=1" });
289
299
  } else {
@@ -91,13 +91,16 @@ try {
91
91
  // dedupe marker, so it's a cheap no-op on every turn except the one right
92
92
  // after a ship. Wrapped + unref'd so it never blocks or breaks the session.
93
93
  try {
94
- const { spawn } = require("child_process");
95
- const child = spawn(
96
- process.execPath,
97
- [path.join(__dirname, "..", "bin", "auto-report.js")],
98
- { cwd: process.cwd(), detached: true, stdio: "ignore" },
99
- );
100
- child.unref();
94
+ const autoReportPath = path.join(__dirname, "..", "bin", "auto-report.js");
95
+ if (fs.existsSync(autoReportPath)) {
96
+ const { spawn } = require("child_process");
97
+ const child = spawn(
98
+ process.execPath,
99
+ [autoReportPath],
100
+ { cwd: process.cwd(), detached: true, stdio: "ignore" },
101
+ );
102
+ child.unref();
103
+ }
101
104
  } catch {}
102
105
 
103
106
  // ── Skip if too soon since last write ────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qualia-framework",
3
- "version": "6.7.1",
3
+ "version": "6.8.1",
4
4
  "description": "Claude Code and Codex workflow framework by Qualia Solutions. Plan, build, verify, ship.",
5
5
  "bin": {
6
6
  "qualia-framework": "./bin/cli.js"
@@ -54,7 +54,11 @@
54
54
  "docs/onboarding.html",
55
55
  "CLAUDE.md",
56
56
  "AGENTS.md",
57
- "guide.md"
57
+ "guide.md",
58
+ "SOUL.md",
59
+ "FLAGS.md",
60
+ "TROUBLESHOOTING.md",
61
+ "CHANGELOG.md"
58
62
  ],
59
63
  "engines": {
60
64
  "node": ">=18"
@@ -33,7 +33,7 @@ A whole-app `/qualia-polish` scores all 9 dimensions across multiple representat
33
33
 
34
34
  | Score | Criteria |
35
35
  |---|---|
36
- | 1 | Inter / Roboto / Arial / system-ui / Space Grotesk as primary. Or single weight throughout. |
36
+ | 1 | Banned font as primary — Inter / Roboto / Arial / Helvetica / system-ui / Space Grotesk / Montserrat / Poppins / Lato / Open Sans. Or single weight throughout. |
37
37
  | 2 | Project font loaded but only one weight, no scale, no letter-spacing variation. |
38
38
  | 3 | Project font with 2-3 weights, basic scale (h1/h2/body), readable. |
39
39
  | 4 | Distinctive display + refined body pair. Letter-spacing varied semantically (tight headlines, open labels). Tabular numerals on numeric data. |
@@ -41,6 +41,8 @@ A whole-app `/qualia-polish` scores all 9 dimensions across multiple representat
41
41
 
42
42
  Evidence format: `font-family: "<name>" used at line N, scale 14/16/24/40 visible, weights 400/500/700`
43
43
 
44
+ The banned-font list above is the machine-enforced counterpart of `bin/slop-detect.mjs` (the static anti-pattern scanner) and `agents/visual-evaluator.md` (the vision evaluator). All three are kept in lockstep — Inter, Roboto, Arial, Helvetica, system-ui, Space Grotesk, Montserrat, Poppins, Lato, Open Sans.
45
+
44
46
  ### 2. Color cohesion
45
47
 
46
48
  | Score | Criteria |
@@ -119,7 +119,7 @@ Read this file (auto-load via skill or `@rules/architecture.md`) when:
119
119
 
120
120
  - Planning a new module or feature with multiple components.
121
121
  - The user requests `/qualia-optimize --deepen` or `--alignment`.
122
- - A verifier is scoring "Container depth & nesting" (per `rules/design-rubric.md` dimension 8).
122
+ - A verifier is scoring "Container depth & nesting" (per `qualia-design/design-rubric.md` dimension 8).
123
123
  - An ADR is being drafted for an architectural fork.
124
124
 
125
125
  Do **not** auto-load this on quick fixes, copy edits, single-component touch-ups — that wastes instruction budget. Use judgment.
@@ -102,7 +102,9 @@ Every skill that spawns an agent must order the prompt from most-stable to most-
102
102
  - Per-task content goes LAST. Mixing task-specific files into `<phase_context>` breaks cache on every spawn within the same wave.
103
103
  - Reference files via `@path` when the harness auto-expands, OR inline the content — but pick one and stick with it per section (switching styles breaks prefix match).
104
104
 
105
- ## Design Quality Rubric (any dimension < 3 = mandatory fix before commit)
105
+ ## Design Quality Rubric (pre-commit SUBSET — any dimension < 3 = mandatory fix before commit)
106
+
107
+ > This 6-dimension table (Typography, Color, Spacing, States, Responsiveness, Accessibility) is the fast pre-commit gate every agent runs inline. It is a **deliberate SUBSET** of the authoritative 9-dimension rubric at `@qualia-design/design-rubric.md` (Typography, Color cohesion, Spatial rhythm, Layout originality, Shadow & depth, Motion intent, Microcopy specificity, Container depth & nesting, Visual system & graphics). For verifier scoring and `/qualia-polish --critique`, the 9-dimension rubric is authoritative — read it, don't score from this subset.
106
108
 
107
109
  | Dimension | 1 (Fail) | 3 (Acceptable) | 5 (Excellent) |
108
110
  |-----------------|-----------------------------------------|------------------------------------------|--------------------------------------------|
package/rules/speed.md CHANGED
@@ -26,14 +26,14 @@ MCP servers impose a **token tax**: their tool definitions consume context-windo
26
26
  - The MCP returns structured JSON that would be expensive to parse from CLI text output.
27
27
  - The MCP enforces governance (RLS-aware queries, scoped DB credentials) that the CLI doesn't.
28
28
 
29
- If a `/skill-name` exists that wraps a CLI, prefer the skill — it's been hardened. Canonical example: `/supabase` skill replaces 32 supabase MCP tools with `supabase` CLI calls and saves substantial token budget.
29
+ If a `/skill-name` exists that wraps a CLI, prefer the skill — it's been hardened. Canonical example: drive Supabase through `npx supabase` (migrations, type generation, local dev, SQL) instead of the Supabase MCP the CLI hits the same API at zero token cost and replaces the bulk of the MCP tool surface (see `infrastructure.md`).
30
30
 
31
31
  ### MCP tier-list (when each is justified)
32
32
 
33
33
  | Server | Always-on? | Justification |
34
34
  |---|---|---|
35
35
  | `claude-in-chrome` | On-demand | Browser automation has no CLI equivalent; use for QA flows only |
36
- | `supabase` MCP | **Off** in favor of `/supabase` skill | CLI covers 95% of operations; MCP only if you need branch management interactively |
36
+ | `supabase` MCP | **Off** in favor of `npx supabase` CLI | CLI covers 95% of operations; MCP only if you need branch management interactively |
37
37
  | `context7` | On-demand | Library docs at runtime — no CLI alternative for Context7 itself |
38
38
  | `notebooklm-mcp` | On-demand | NotebookLM has no CLI; only loaded when researching against existing notebooks |
39
39
  | `firecrawl-mcp` | On-demand | Web scraping; only loaded when feature requires it |
@@ -207,13 +207,13 @@ With all three reports + `<user_confusion>` + `<session_context>` in hand, produ
207
207
  |---|---|
208
208
  | Plan says built but code has stubs | `/qualia-plan {N} --gaps` → `/qualia-build` → `/qualia-verify` |
209
209
  | Verify FAILed and no postmortem ran | `/qualia-postmortem` → `/qualia-plan {N} --gaps` → `/qualia-build` |
210
- | Stale `.continue-here.md`, ongoing context | `/qualia-resume` `/qualia` |
210
+ | Stale `.continue-here.md`, ongoing context | `/qualia` (restores from `.continue-here.md` / STATE.md) |
211
211
  | Brownfield drift (plan and code diverged hard) | `/qualia-map` → `/qualia-plan {N} --gaps` |
212
212
  | Phase context missing (no `/qualia-scope` ran) | `/qualia-scope {N}` → `/qualia-plan {N}` |
213
213
  | Specific error, scope clear | `/qualia-fix '<symptom>'` |
214
214
  | Performance feels off, no profile | `/qualia-fix --perf '<route>'` or `/qualia-optimize --perf` |
215
215
  | Design feels off | `/qualia-polish --critique` then `/qualia-polish` |
216
- | User is overwhelmed | `/qualia-pause` (save handoff), come back later |
216
+ | User is overwhelmed | `/qualia-handoff` (save handoff), come back later |
217
217
  | Truly nothing actionable found | Ask one specific question; don't invent a sequence |
218
218
 
219
219
  Pick the sequence that fits the actual evidence. Substitute real `{N}` from the Plan-view scan.
@@ -240,6 +240,6 @@ node ${QUALIA_BIN}/qualia-ui.js end "DIAGNOSED" "{first command in the sequence,
240
240
  - User knows what they're doing and just wants the next command → `/qualia`
241
241
  - User has a specific error message they want fixed → `/qualia-fix '<symptom>'`
242
242
  - User wants to review code quality → `/qualia-review`
243
- - User wants to pause and come back → `/qualia-pause`
243
+ - User wants to pause and come back → `/qualia-handoff`
244
244
 
245
245
  `/qualia-idk` is specifically for **"I'm not sure what I'm even looking at"** situations. Route to sharper tools when the question is sharper.