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/CHANGELOG.md +2314 -0
- package/FLAGS.md +73 -0
- package/SOUL.md +17 -0
- package/TROUBLESHOOTING.md +183 -0
- package/agents/plan-checker.md +4 -0
- package/agents/planner.md +8 -0
- package/agents/qa-browser.md +6 -2
- package/agents/research-synthesizer.md +4 -0
- package/agents/researcher.md +4 -0
- package/agents/roadmapper.md +4 -0
- package/agents/verifier.md +1 -1
- package/agents/visual-evaluator.md +8 -6
- package/bin/cli.js +7 -1
- package/bin/install.js +122 -15
- package/bin/runtime-manifest.js +1 -0
- package/bin/security-scan.js +24 -10
- package/bin/trust-score.js +34 -0
- package/hooks/migration-guard.js +4 -4
- package/hooks/pre-deploy-gate.js +14 -4
- package/hooks/stop-session-log.js +10 -7
- package/package.json +6 -2
- package/qualia-design/design-rubric.md +3 -1
- package/rules/architecture.md +1 -1
- package/rules/grounding.md +3 -1
- package/rules/speed.md +2 -2
- package/skills/qualia-idk/SKILL.md +3 -3
- package/skills/qualia-polish/REFERENCE.md +11 -6
- package/skills/qualia-polish/SKILL.md +20 -3
- package/skills/qualia-polish/scripts/loop.mjs +24 -6
- package/skills/qualia-polish/scripts/playwright-capture.mjs +89 -11
- package/skills/qualia-polish/scripts/vibe-tokens.mjs +57 -1
- package/skills/qualia-research/SKILL.md +1 -1
- package/skills/qualia-road/SKILL.md +6 -0
- package/skills/qualia-scope/SKILL.md +2 -2
- package/skills/qualia-secure/SKILL.md +5 -5
- package/templates/knowledge/index.md +4 -4
- package/tests/bin.test.sh +4 -4
- package/tests/lib.test.sh +2 -2
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
|
|
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
|
-
|
|
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 = [
|
|
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
|
|
793
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 },
|
package/bin/runtime-manifest.js
CHANGED
|
@@ -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)" },
|
package/bin/security-scan.js
CHANGED
|
@@ -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 (
|
|
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(
|
|
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(
|
|
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(
|
|
412
|
+
process.exit(exitCode);
|
|
399
413
|
}
|
|
400
414
|
|
|
401
415
|
module.exports = { main, scanContent, defaultPaths, categoryScore, renderDeepPromptPack, SECRET_PATTERNS, CONFIG_SMELLS };
|
package/bin/trust-score.js
CHANGED
|
@@ -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,
|
package/hooks/migration-guard.js
CHANGED
|
@@ -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.
|
|
164
|
+
console.error("⬢ Migration guard — dangerous patterns found:");
|
|
165
165
|
for (const e of errors) {
|
|
166
|
-
console.
|
|
166
|
+
console.error(` ✗ ${e}`);
|
|
167
167
|
}
|
|
168
|
-
console.
|
|
169
|
-
console.
|
|
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
|
}
|
package/hooks/pre-deploy-gate.js
CHANGED
|
@@ -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:
|
|
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)
|
|
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
|
-
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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.
|
|
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
|
|
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 |
|
package/rules/architecture.md
CHANGED
|
@@ -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 `
|
|
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.
|
package/rules/grounding.md
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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.
|