qualia-framework 6.2.10 → 6.4.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 (78) hide show
  1. package/AGENTS.md +8 -7
  2. package/CLAUDE.md +5 -4
  3. package/README.md +27 -56
  4. package/bin/cli.js +113 -18
  5. package/bin/command-surface.js +75 -0
  6. package/bin/harness-eval.js +296 -0
  7. package/bin/install.js +43 -31
  8. package/bin/knowledge-flush.js +21 -10
  9. package/bin/knowledge.js +1 -1
  10. package/bin/learning-candidates.js +217 -0
  11. package/bin/project-snapshot.js +20 -0
  12. package/bin/prune-deprecated.js +64 -0
  13. package/bin/report-payload.js +18 -0
  14. package/bin/runtime-manifest.js +7 -0
  15. package/bin/security-scan.js +409 -0
  16. package/bin/state.js +31 -0
  17. package/bin/status-snapshot.js +363 -0
  18. package/bin/trust-score.js +3 -11
  19. package/bin/work-packet.js +228 -0
  20. package/docs/erp-contract.md +81 -1
  21. package/docs/onboarding.html +0 -11
  22. package/guide.md +15 -38
  23. package/hooks/fawzi-approval-guard.js +143 -0
  24. package/hooks/pre-compact.js +232 -0
  25. package/hooks/pre-deploy-gate.js +74 -1
  26. package/hooks/session-start.js +29 -1
  27. package/package.json +1 -1
  28. package/qualia-design/frontend.md +2 -2
  29. package/rules/codex-goal.md +1 -1
  30. package/rules/one-opinion.md +2 -2
  31. package/rules/speed.md +0 -1
  32. package/skills/qualia/SKILL.md +4 -4
  33. package/skills/qualia-build/SKILL.md +1 -1
  34. package/skills/qualia-discuss/SKILL.md +1 -1
  35. package/skills/qualia-doctor/SKILL.md +1 -1
  36. package/skills/qualia-feature/SKILL.md +2 -2
  37. package/skills/qualia-fix/SKILL.md +4 -4
  38. package/skills/qualia-idk/SKILL.md +133 -54
  39. package/skills/qualia-learn/SKILL.md +2 -2
  40. package/skills/qualia-map/SKILL.md +1 -1
  41. package/skills/qualia-milestone/SKILL.md +1 -1
  42. package/skills/qualia-new/SKILL.md +1 -1
  43. package/skills/qualia-optimize/SKILL.md +1 -1
  44. package/skills/qualia-plan/SKILL.md +1 -1
  45. package/skills/qualia-polish/REFERENCE.md +1 -1
  46. package/skills/qualia-polish/SKILL.md +19 -4
  47. package/skills/{qualia-vibe/scripts/extract.mjs → qualia-polish/scripts/vibe-extract.mjs} +4 -4
  48. package/skills/{qualia-vibe/scripts/tokens.mjs → qualia-polish/scripts/vibe-tokens.mjs} +6 -6
  49. package/skills/qualia-postmortem/SKILL.md +1 -1
  50. package/skills/qualia-report/SKILL.md +1 -1
  51. package/skills/qualia-research/SKILL.md +1 -1
  52. package/skills/qualia-review/SKILL.md +1 -1
  53. package/skills/qualia-road/SKILL.md +15 -20
  54. package/skills/qualia-secure/SKILL.md +105 -0
  55. package/skills/qualia-ship/SKILL.md +12 -5
  56. package/skills/qualia-test/SKILL.md +1 -1
  57. package/skills/qualia-verify/SKILL.md +10 -2
  58. package/skills/zoho-workflow/SKILL.md +1 -1
  59. package/templates/help.html +1 -12
  60. package/tests/bin.test.sh +147 -75
  61. package/tests/hooks.test.sh +81 -1
  62. package/tests/install-smoke.test.sh +14 -4
  63. package/tests/lib.test.sh +145 -3
  64. package/tests/published-install-smoke.test.sh +5 -4
  65. package/tests/refs.test.sh +32 -20
  66. package/tests/runner.js +30 -29
  67. package/tests/state.test.sh +106 -7
  68. package/skills/qualia-debug/SKILL.md +0 -193
  69. package/skills/qualia-flush/SKILL.md +0 -198
  70. package/skills/qualia-help/SKILL.md +0 -74
  71. package/skills/qualia-hook-gen/SKILL.md +0 -206
  72. package/skills/qualia-issues/SKILL.md +0 -151
  73. package/skills/qualia-pause/SKILL.md +0 -68
  74. package/skills/qualia-resume/SKILL.md +0 -52
  75. package/skills/qualia-skill-new/SKILL.md +0 -173
  76. package/skills/qualia-triage/SKILL.md +0 -152
  77. package/skills/qualia-vibe/SKILL.md +0 -229
  78. package/skills/qualia-zoom/SKILL.md +0 -51
@@ -0,0 +1,296 @@
1
+ #!/usr/bin/env node
2
+ // Qualia harness eval — deterministic project scoring + evidence artifact.
3
+ //
4
+ // This is the mechanical layer above trust-score and contract-runner. It writes
5
+ // one JSON/Markdown artifact that can be attached to reports, snapshots, and
6
+ // state transitions instead of relying on a prose "looks good" verdict.
7
+
8
+ const fs = require("fs");
9
+ const path = require("path");
10
+ const { spawnSync } = require("child_process");
11
+ const pc = require("./plan-contract.js");
12
+ const contractRunner = require("./contract-runner.js");
13
+ const trust = require("./trust-score.js");
14
+
15
+ function parseArgs(argv) {
16
+ const args = { cwd: process.cwd() };
17
+ for (let i = 2; i < argv.length; i++) {
18
+ const a = argv[i];
19
+ if (a === "--json") args.json = true;
20
+ else if (a === "--run") args.run = true;
21
+ else if (a === "--write") args.write = true;
22
+ else if (a === "--no-write") args.no_write = true;
23
+ else if (a === "--phase") args.phase = Number(argv[++i]);
24
+ else if (a.startsWith("--phase=")) args.phase = Number(a.slice("--phase=".length));
25
+ else if (a === "--cwd") args.cwd = argv[++i];
26
+ else if (a.startsWith("--cwd=")) args.cwd = a.slice("--cwd=".length);
27
+ }
28
+ return args;
29
+ }
30
+
31
+ function readJson(file, fallback = null) {
32
+ try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return fallback; }
33
+ }
34
+
35
+ function readText(file, fallback = "") {
36
+ try { return fs.readFileSync(file, "utf8"); } catch { return fallback; }
37
+ }
38
+
39
+ function rel(root, file) {
40
+ return path.relative(root, file).replace(/\\/g, "/");
41
+ }
42
+
43
+ function stateCheck(cwd) {
44
+ const r = spawnSync(process.execPath, [path.join(__dirname, "state.js"), "check"], {
45
+ cwd,
46
+ encoding: "utf8",
47
+ stdio: ["ignore", "pipe", "pipe"],
48
+ timeout: 5000,
49
+ });
50
+ const parsed = readJsonFromString(r.stdout);
51
+ return {
52
+ ok: r.status === 0 && parsed && parsed.ok === true,
53
+ status: r.status,
54
+ stdout: r.stdout,
55
+ stderr: r.stderr,
56
+ parsed,
57
+ };
58
+ }
59
+
60
+ function readJsonFromString(text) {
61
+ try { return JSON.parse(text); } catch { return null; }
62
+ }
63
+
64
+ function latestEval(cwd) {
65
+ const dir = path.join(cwd, ".planning", "evals");
66
+ try {
67
+ const files = fs.readdirSync(dir)
68
+ .filter((f) => /^harness-eval-.*\.json$/.test(f))
69
+ .map((f) => path.join(dir, f))
70
+ .sort();
71
+ if (!files.length) return null;
72
+ return readJson(files[files.length - 1], null);
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ function addCheck(checks, name, weight, status, evidence, data) {
79
+ checks.push({
80
+ name,
81
+ weight,
82
+ status,
83
+ score: status === "pass" || status === "not_applicable" ? weight : status === "warn" ? Math.floor(weight / 2) : 0,
84
+ evidence,
85
+ ...(data && typeof data === "object" ? data : {}),
86
+ });
87
+ }
88
+
89
+ function verificationVerdict(text) {
90
+ if (!text) return "";
91
+ const explicit = text.match(/^result:\s*(PASS|FAIL)$/im);
92
+ if (explicit) return explicit[1].toLowerCase();
93
+ const md = text.match(/\bVerdict\b.*\b(PASS|FAIL)\b/i);
94
+ if (md) return md[1].toLowerCase();
95
+ if (/PHASE\s+PASS|ALL\s+CRITERIA\s+PASSED/i.test(text)) return "pass";
96
+ if (/PHASE\s+FAIL|FAILED\s+CRITERIA|GAPS\s+FOUND/i.test(text)) return "fail";
97
+ return "";
98
+ }
99
+
100
+ function writeArtifacts(cwd, result) {
101
+ const dir = path.join(cwd, ".planning", "evals");
102
+ fs.mkdirSync(dir, { recursive: true });
103
+ const stamp = result.generated_at.replace(/[:.]/g, "-");
104
+ const jsonPath = path.join(dir, `harness-eval-${stamp}.json`);
105
+ const mdPath = path.join(dir, `harness-eval-${stamp}.md`);
106
+ fs.writeFileSync(jsonPath, JSON.stringify(result, null, 2) + "\n");
107
+ const lines = [
108
+ `# Harness Eval`,
109
+ ``,
110
+ `**Generated:** ${result.generated_at}`,
111
+ `**Phase:** ${result.phase || "n/a"}`,
112
+ `**Status:** ${result.status}`,
113
+ `**Score:** ${result.score}/100`,
114
+ ``,
115
+ `## Checks`,
116
+ ``,
117
+ `| Check | Status | Score | Evidence |`,
118
+ `|---|---:|---:|---|`,
119
+ ...result.checks.map((c) => `| ${c.name} | ${c.status} | ${c.score}/${c.weight} | ${String(c.evidence || "").replace(/\|/g, "\\|")} |`),
120
+ ``,
121
+ ];
122
+ fs.writeFileSync(mdPath, lines.join("\n"));
123
+ result.artifacts = {
124
+ json: rel(cwd, jsonPath),
125
+ markdown: rel(cwd, mdPath),
126
+ };
127
+ fs.writeFileSync(jsonPath, JSON.stringify(result, null, 2) + "\n");
128
+ return result.artifacts;
129
+ }
130
+
131
+ function buildHarnessEval(options = {}) {
132
+ const cwd = path.resolve(options.cwd || process.cwd());
133
+ const planning = path.join(cwd, ".planning");
134
+ const generatedAt = options.now || new Date().toISOString();
135
+ const checks = [];
136
+
137
+ const state = stateCheck(cwd);
138
+ if (!fs.existsSync(planning)) {
139
+ addCheck(checks, "planning_state", 15, "fail", "No .planning directory; run /qualia-new");
140
+ return finalize({ cwd, generatedAt, phase: options.phase || 0, checks, statusOverride: "FAIL" }, options);
141
+ }
142
+
143
+ const tracking = readJson(path.join(planning, "tracking.json"), {});
144
+ const phase = Number(options.phase || state.parsed?.phase || tracking.phase || 0);
145
+ addCheck(
146
+ checks,
147
+ "planning_state",
148
+ 15,
149
+ state.ok ? "pass" : "fail",
150
+ state.ok ? "state.js check returned ok" : "state.js check failed",
151
+ { state: state.parsed || null }
152
+ );
153
+
154
+ const ledgerResult = (() => {
155
+ try {
156
+ const stateLedger = require("./state-ledger.js");
157
+ return stateLedger.validate(cwd);
158
+ } catch (e) {
159
+ return { ok: false, errors: [e.message] };
160
+ }
161
+ })();
162
+ addCheck(
163
+ checks,
164
+ "state_ledger",
165
+ 10,
166
+ ledgerResult.ok ? "pass" : "fail",
167
+ ledgerResult.ok ? `${ledgerResult.count || 0} hash-chained event(s)` : (ledgerResult.errors || []).join("; ")
168
+ );
169
+
170
+ const planPath = path.join(planning, `phase-${phase}-plan.md`);
171
+ const contractPath = path.join(planning, `phase-${phase}-contract.json`);
172
+ const hasPlan = fs.existsSync(planPath);
173
+ const hasContract = fs.existsSync(contractPath);
174
+ let loadedContract = null;
175
+
176
+ if (!hasPlan && !hasContract) {
177
+ addCheck(checks, "plan_contract", 20, "not_applicable", `No current phase plan/contract for phase ${phase}`);
178
+ } else if (!hasContract) {
179
+ addCheck(checks, "plan_contract", 20, "fail", `Missing ${rel(cwd, contractPath)}`);
180
+ } else {
181
+ const loaded = pc.readContractFile(contractPath);
182
+ loadedContract = loaded.ok ? loaded.contract : null;
183
+ const errors = loaded.ok ? pc.validate(loaded.contract) : [loaded.message || loaded.error];
184
+ const drift = hasPlan ? pc.checkDrift(contractPath, planPath) : { ok: true, drift: false };
185
+ const ok = loaded.ok && errors.length === 0 && !(drift.ok && drift.drift);
186
+ addCheck(
187
+ checks,
188
+ "plan_contract",
189
+ 20,
190
+ ok ? "pass" : "fail",
191
+ ok ? `${rel(cwd, contractPath)} valid and in sync` : [...errors, drift.drift ? "contract drifted from plan" : ""].filter(Boolean).join("; "),
192
+ { contract: rel(cwd, contractPath) }
193
+ );
194
+ }
195
+
196
+ const evidencePath = path.join(planning, "evidence", `phase-${phase}-contract-run.json`);
197
+ let evidence = readJson(evidencePath, null);
198
+ if (loadedContract && options.run) {
199
+ evidence = contractRunner.runContract(loadedContract, { cwd });
200
+ }
201
+ if (!loadedContract) {
202
+ addCheck(checks, "machine_evidence", 20, "not_applicable", "No contract to execute");
203
+ } else if (evidence && evidence.ok === true) {
204
+ addCheck(checks, "machine_evidence", 20, "pass", `${rel(cwd, evidencePath)} passed ${evidence.checked || 0} check(s)`);
205
+ } else {
206
+ addCheck(
207
+ checks,
208
+ "machine_evidence",
209
+ 20,
210
+ "fail",
211
+ evidence ? `${evidence.failed || "unknown"} failing machine check(s)` : `Missing ${rel(cwd, evidencePath)}`
212
+ );
213
+ }
214
+
215
+ const verificationPath = path.join(planning, `phase-${phase}-verification.md`);
216
+ const verification = readText(verificationPath, "");
217
+ const ieCount = (verification.match(/INSUFFICIENT EVIDENCE/g) || []).length;
218
+ const verdict = verificationVerdict(verification);
219
+ if (!verification) {
220
+ addCheck(checks, "verification_report", 15, "fail", `Missing ${rel(cwd, verificationPath)}`);
221
+ } else if (ieCount > 0) {
222
+ addCheck(checks, "verification_report", 15, "fail", `${ieCount} insufficient-evidence marker(s)`);
223
+ } else if (verdict === "pass" || verdict === "fail") {
224
+ addCheck(checks, "verification_report", 15, verdict === "pass" ? "pass" : "warn", `${rel(cwd, verificationPath)} verdict=${verdict}`);
225
+ } else {
226
+ addCheck(checks, "verification_report", 15, "warn", `${rel(cwd, verificationPath)} has no machine-readable verdict`);
227
+ }
228
+
229
+ const trustScore = trust.buildTrustScore(cwd);
230
+ addCheck(
231
+ checks,
232
+ "framework_trust_score",
233
+ 10,
234
+ trustScore.status === "FAIL" ? "fail" : trustScore.status === "DEGRADED" ? "warn" : "pass",
235
+ `trust-score=${trustScore.score}/100 status=${trustScore.status}`,
236
+ { trust_score: trustScore.score }
237
+ );
238
+
239
+ const hasErpId = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(tracking.erp_project_id || "");
240
+ const hasProjectKey = !!(tracking.project_id || tracking.project);
241
+ addCheck(
242
+ checks,
243
+ "erp_linkage",
244
+ 10,
245
+ hasErpId ? "pass" : hasProjectKey ? "warn" : "fail",
246
+ hasErpId ? "tracking.json has erp_project_id UUID" : hasProjectKey ? "project key exists, ERP UUID missing" : "missing project/project_id for ERP correlation"
247
+ );
248
+
249
+ return finalize({ cwd, generatedAt, phase, checks }, options);
250
+ }
251
+
252
+ function finalize(base, options) {
253
+ const score = Math.max(0, Math.min(100, base.checks.reduce((n, c) => n + (c.score || 0), 0)));
254
+ const failed = base.checks.filter((c) => c.status === "fail").length;
255
+ const warned = base.checks.filter((c) => c.status === "warn").length;
256
+ const status = base.statusOverride || (failed ? "FAIL" : score >= 85 ? "PASS" : warned ? "WARN" : "PASS");
257
+ const result = {
258
+ ok: status !== "FAIL",
259
+ status,
260
+ score,
261
+ phase: base.phase,
262
+ generated_at: base.generatedAt,
263
+ checks: base.checks,
264
+ };
265
+ if (options.write && !options.no_write && fs.existsSync(path.join(base.cwd, ".planning"))) {
266
+ writeArtifacts(base.cwd, result);
267
+ }
268
+ return result;
269
+ }
270
+
271
+ function printHuman(result) {
272
+ console.log(`Harness eval: ${result.score}/100 (${result.status})`);
273
+ for (const c of result.checks) {
274
+ console.log(`${c.name}: ${c.status} (${c.score}/${c.weight}) — ${c.evidence}`);
275
+ }
276
+ if (result.artifacts) {
277
+ console.log(`Artifacts: ${result.artifacts.json}, ${result.artifacts.markdown}`);
278
+ }
279
+ }
280
+
281
+ function main(argv) {
282
+ const args = parseArgs(argv);
283
+ const result = buildHarnessEval(args);
284
+ if (args.json) console.log(JSON.stringify(result, null, 2));
285
+ else printHuman(result);
286
+ return result.ok ? 0 : 1;
287
+ }
288
+
289
+ module.exports = {
290
+ buildHarnessEval,
291
+ latestEval,
292
+ verificationVerdict,
293
+ };
294
+
295
+ if (require.main === module) process.exit(main(process.argv));
296
+
package/bin/install.js CHANGED
@@ -5,6 +5,7 @@ const path = require("path");
5
5
  const fs = require("fs");
6
6
  const ui = require("./qualia-ui.js");
7
7
  const { RUNTIME_BIN_SCRIPTS, binFiles } = require("./runtime-manifest.js");
8
+ const { ACTIVE_SKILLS, RETIRED_SKILLS } = require("./command-surface.js");
8
9
  const { renderText } = require("./host-adapters.js");
9
10
 
10
11
  // ─── Colors (kept for legacy log lines; new sections route through qualia-ui) ─
@@ -49,7 +50,7 @@ const installStart = Date.now();
49
50
 
50
51
  // ─── Team codes ──────────────────────────────────────────
51
52
  const DEFAULT_TEAM = {
52
- "QS-FAWZI-01": {
53
+ "QS-FAWZI-11": {
53
54
  name: "Fawzi Goussous",
54
55
  role: "OWNER",
55
56
  description: "Company owner. Full access. Can push to main, approve deploys, edit secrets.",
@@ -275,11 +276,7 @@ function renderCodexAgentToml(markdown, filenameFallback) {
275
276
  // Pruned from BOTH ~/.claude/skills/ and ~/.codex/skills/ on every install run
276
277
  // so the active surface matches what the framework currently ships.
277
278
  const DEPRECATED_SKILLS = [
278
- "qualia-task", // v5.7.0 — folded into qualia-feature
279
- "qualia-quick", // v5.7.0 — folded into qualia-feature
280
- "qualia-polish-loop", // v5.8.0 — folded into qualia-polish --loop
281
- "qualia-design", // v4 wave 2 — folded into scope-adaptive qualia-polish
282
- "qualia-prd", // v5.8.0 — surface cleanup
279
+ ...RETIRED_SKILLS,
283
280
  ];
284
281
 
285
282
  function pruneDeprecatedSkills(baseDir) {
@@ -491,8 +488,8 @@ function targetLabel(t) {
491
488
  }
492
489
 
493
490
  // ─── Resolve team code (tolerates case + O/0 typo in suffix) ─
494
- // Accepts "qs-fawzi-01", "QS-FAWZI-01", "QS-FAWZI-O1" (letter O in the
495
- // numeric suffix), and returns the canonical key if found, else null.
491
+ // Accepts lowercase codes and common letter-O typos in numeric suffixes,
492
+ // then returns the canonical key if found, else null.
496
493
  // Only normalizes O→0 in the segment AFTER the last dash — "QS-MOAYAD-03"
497
494
  // contains a real "O" in the name and must not be mangled.
498
495
  function resolveTeamCode(input) {
@@ -521,7 +518,7 @@ async function main() {
521
518
  if (!member) {
522
519
  console.log("");
523
520
  log(`${RED}✗${RESET} Invalid code: "${rawCode}". Get your install code from Fawzi.`);
524
- log(`${DIM} Tip: codes use digit zero, not letter O. Format: QS-NAME-01${RESET}`);
521
+ log(`${DIM} Tip: codes use digit zero, not letter O. Format: QS-NAME-##${RESET}`);
525
522
  console.log("");
526
523
  process.exit(1);
527
524
  }
@@ -549,9 +546,7 @@ async function main() {
549
546
 
550
547
  // ─── Skills ──────────────────────────────────────────
551
548
  const skillsDir = path.join(FRAMEWORK_DIR, "skills");
552
- const skills = fs
553
- .readdirSync(skillsDir)
554
- .filter((d) => fs.statSync(path.join(skillsDir, d)).isDirectory());
549
+ const skills = ACTIVE_SKILLS.filter((d) => fs.existsSync(path.join(skillsDir, d, "SKILL.md")));
555
550
 
556
551
  printSection("Skills");
557
552
  const claudePruned = pruneDeprecatedSkills(CLAUDE_DIR);
@@ -666,12 +661,18 @@ async function main() {
666
661
  } catch {}
667
662
  // Purge deprecated hooks from existing installs on upgrade.
668
663
  // - block-env-edit.js (v3.2.0): team now has full read/write on .env*.
669
- // - pre-compact.js (v6.2.0): bot-committed STATE.md + tracking.json on
670
- // context compaction for ERP visibility. ERP never read tracking.json
671
- // from git, and state.js already provides crash-safe atomic writes with
672
- // a write-ahead journal (state.js:36-64) the bot commit added no
673
- // durability, just history noise.
674
- const DEPRECATED_HOOKS = ["block-env-edit.js", "pre-compact.js"];
664
+ //
665
+ // Note: pre-compact.js was removed in v6.2.0 (it bot-committed STATE.md +
666
+ // tracking.json, which added no durability over state.js's atomic writes
667
+ // + journal). v6.3.2 reintroduces pre-compact.js but with a fundamentally
668
+ // different mechanism — it writes a markdown SIDECAR to
669
+ // .planning/.compaction-snapshot.md (no git, no state.js writes) so the
670
+ // next session can see what was in flight when compaction wiped context.
671
+ // Safe to ship as a fresh install; the v6.2.0 stripping logic in cli.js
672
+ // doctor remains, but only strips the OLD legacy command (which our v2
673
+ // hook does not match — different content, same filename is fine because
674
+ // install always overwrites).
675
+ const DEPRECATED_HOOKS = ["block-env-edit.js"];
675
676
  for (const f of DEPRECATED_HOOKS) {
676
677
  const p = path.join(hooksDest, f);
677
678
  try { if (fs.existsSync(p)) fs.unlinkSync(p); } catch {}
@@ -868,7 +869,7 @@ Recurring issues and their solutions.
868
869
  ## Install code "Invalid" — user typed letter O instead of digit 0
869
870
  **Symptom:** \`npx qualia-framework install\` rejects \`QS-NAME-O1\` (letter O in suffix).
870
871
  **Cause:** Team codes use digit zero (\`-01\`, \`-02\`, etc.), not letter O.
871
- **Fix:** Since v2.8.1, install.js auto-normalizes: \`QS-FAWZI-O1\` → \`QS-FAWZI-01\`. The normalization only touches the segment after the last dash, so \`QS-MOAYAD-03\` (real O in name) is preserved.
872
+ **Fix:** Since v2.8.1, install.js auto-normalizes: \`QS-HASAN-O2\` → \`QS-HASAN-02\`. The normalization only touches the segment after the last dash, so \`QS-MOAYAD-03\` (real O in name) is preserved.
872
873
  **Framework version:** Fixed in v2.8.1.
873
874
 
874
875
  ---
@@ -1061,6 +1062,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
1061
1062
  "session-start.js", "auto-update.js", "branch-guard.js", "pre-push.js",
1062
1063
  "pre-deploy-gate.js", "migration-guard.js", "pre-compact.js",
1063
1064
  "git-guardrails.js", "stop-session-log.js",
1065
+ "fawzi-approval-guard.js",
1064
1066
  // v5.0 — insights-driven destructive-op + wrong-account guards
1065
1067
  "vercel-account-guard.js", "env-empty-guard.js", "supabase-destructive-guard.js",
1066
1068
  ]);
@@ -1086,6 +1088,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
1086
1088
  hooks: [
1087
1089
  { type: "command", command: nodeCmd("auto-update.js"), timeout: 5 },
1088
1090
  { type: "command", command: nodeCmd("git-guardrails.js"), timeout: 5, statusMessage: "⬢ Checking git safety..." },
1091
+ { type: "command", command: nodeCmd("fawzi-approval-guard.js"), timeout: 5 },
1089
1092
  { type: "command", if: "Bash(git push*)", command: nodeCmd("branch-guard.js"), timeout: 5, statusMessage: "⬢ Checking branch permissions..." },
1090
1093
  { type: "command", if: "Bash(git push*)", command: nodeCmd("pre-push.js"), timeout: 15, statusMessage: "⬢ Syncing tracking..." },
1091
1094
  { type: "command", if: "Bash(vercel --prod*)", command: nodeCmd("pre-deploy-gate.js"), timeout: 180, statusMessage: "⬢ Running quality gates..." },
@@ -1098,15 +1101,25 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
1098
1101
  {
1099
1102
  matcher: "Edit|Write",
1100
1103
  hooks: [
1104
+ { type: "command", command: nodeCmd("fawzi-approval-guard.js"), timeout: 5 },
1101
1105
  { type: "command", if: "Edit(*migration*)|Write(*migration*)|Edit(*.sql)|Write(*.sql)", command: nodeCmd("migration-guard.js"), timeout: 10, statusMessage: "⬢ Checking migration safety..." },
1102
1106
  ],
1103
1107
  },
1104
1108
  ],
1105
- // v6.2.0: PreCompact intentionally empty. The qualiaHooks loop in the
1106
- // settings.json merge below still iterates over this key, so legacy
1107
- // Qualia-owned pre-compact.js entries get stripped from existing user
1108
- // settings on upgrade. Nothing new is wired in.
1109
- PreCompact: [],
1109
+ // v6.3.2: PreCompact reintroduced with a NEW mechanism. The old hook
1110
+ // (removed in v6.2.0) bot-committed STATE.md + tracking.json. This v2
1111
+ // hook writes a markdown SIDECAR to .planning/.compaction-snapshot.md
1112
+ // (no git, no state.js writes). The qualiaHooks loop below still
1113
+ // iterates over this key, so any leftover legacy pre-compact.js wiring
1114
+ // is replaced by the new one cleanly.
1115
+ PreCompact: [
1116
+ {
1117
+ matcher: ".*",
1118
+ hooks: [
1119
+ { type: "command", command: nodeCmd("pre-compact.js"), timeout: 10 },
1120
+ ],
1121
+ },
1122
+ ],
1110
1123
  Stop: [
1111
1124
  {
1112
1125
  matcher: ".*",
@@ -1171,7 +1184,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
1171
1184
  fs.writeFileSync(settingsTmp, JSON.stringify(settings, null, 2));
1172
1185
  fs.renameSync(settingsTmp, settingsPath);
1173
1186
 
1174
- ok("Hooks: session-start, auto-update, branch-guard, pre-push, migration-guard, deploy-gate, git-guardrails, stop-session-log, vercel-account-guard, env-empty-guard, supabase-destructive-guard");
1187
+ ok("Hooks: session-start, auto-update, branch-guard, pre-push, migration-guard, deploy-gate, git-guardrails, stop-session-log, fawzi-approval-guard, vercel-account-guard, env-empty-guard, supabase-destructive-guard");
1175
1188
  ok("Status line + spinner configured");
1176
1189
  ok("Environment variables + permissions");
1177
1190
 
@@ -1208,10 +1221,7 @@ function printSummary({ member, target, claudeInstalled }) {
1208
1221
  const hooksSource = path.join(FRAMEWORK_DIR, "hooks");
1209
1222
  const rulesDir = path.join(FRAMEWORK_DIR, "rules");
1210
1223
  const tmplDir = path.join(FRAMEWORK_DIR, "templates");
1211
- const skillsDir = path.join(FRAMEWORK_DIR, "skills");
1212
- const skillCount = fs
1213
- .readdirSync(skillsDir)
1214
- .filter((d) => fs.statSync(path.join(skillsDir, d)).isDirectory()).length;
1224
+ const skillCount = ACTIVE_SKILLS.length;
1215
1225
  const agentCount = fs.readdirSync(agentsDir).filter((f) => f.endsWith(".md")).length;
1216
1226
  const hookCount = fs.readdirSync(hooksSource).length;
1217
1227
  const ruleCount = fs.readdirSync(rulesDir).length;
@@ -1450,9 +1460,9 @@ async function installCodex(member, target) {
1450
1460
  const skillsDest = path.join(CODEX_DIR, "skills");
1451
1461
  const codexPruned = pruneDeprecatedSkills(CODEX_DIR);
1452
1462
  for (const name of codexPruned) ok(`pruned deprecated: ${name}`);
1453
- for (const skill of fs.readdirSync(skillsSrc)) {
1463
+ for (const skill of ACTIVE_SKILLS) {
1454
1464
  const src = path.join(skillsSrc, skill);
1455
- if (!fs.statSync(src).isDirectory()) continue;
1465
+ if (!fs.existsSync(src) || !fs.statSync(src).isDirectory()) continue;
1456
1466
  copyTreeTransform(src, path.join(skillsDest, skill), codexText);
1457
1467
  }
1458
1468
  ok("skills/");
@@ -1529,6 +1539,7 @@ async function installCodex(member, target) {
1529
1539
  hooks: [
1530
1540
  { type: "command", command: nodeCmd("auto-update.js"), timeout: 5, statusMessage: "Qualia update check..." },
1531
1541
  { type: "command", command: nodeCmd("git-guardrails.js"), timeout: 5, statusMessage: "Qualia git safety..." },
1542
+ { type: "command", command: nodeCmd("fawzi-approval-guard.js"), timeout: 5 },
1532
1543
  { type: "command", command: nodeCmd("branch-guard.js"), timeout: 5 },
1533
1544
  { type: "command", command: nodeCmd("pre-push.js"), timeout: 15 },
1534
1545
  { type: "command", command: nodeCmd("pre-deploy-gate.js"), timeout: 180 },
@@ -1540,6 +1551,7 @@ async function installCodex(member, target) {
1540
1551
  {
1541
1552
  matcher: "Edit|Write",
1542
1553
  hooks: [
1554
+ { type: "command", command: nodeCmd("fawzi-approval-guard.js"), timeout: 5 },
1543
1555
  { type: "command", command: nodeCmd("migration-guard.js"), timeout: 10 },
1544
1556
  ],
1545
1557
  },
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  // ~/.claude/bin/knowledge-flush.js — non-interactive memory-layer flush.
3
3
  //
4
- // Wraps `/qualia-flush` so it can run from cron (or systemd timer, or any
5
- // CI/scheduled job) without an interactive Claude Code session. Closes the
4
+ // Runs the Qualia memory flush prompt from cron (or systemd timer, or any
5
+ // CI/scheduled job) without installing a separate slash command. Closes the
6
6
  // memory loop end-to-end:
7
7
  //
8
8
  // Stop hook (auto, every turn) → <install-home>/knowledge/daily-log/{date}.md
9
9
  // THIS SCRIPT (weekly cron) → spawns the installed agent CLI
10
- // /qualia-flush → promotes raw → curated tier
10
+ // flush prompt → promotes raw → curated tier
11
11
  // bin/knowledge.js (every spawn) → reads index.md → reaches the right file
12
12
  //
13
13
  // Usage:
@@ -86,10 +86,9 @@ function which(cmd) {
86
86
  return null;
87
87
  }
88
88
 
89
- // Pass-through args (so `--days 14`, `--dry-run`, `--project X` all reach the
90
- // skill). We don't parse them ourselves the skill is the source of truth
91
- // for argument semantics. We only use `--days` locally to short-circuit when
92
- // the daily-log is genuinely empty.
89
+ // Pass-through args (so `--days 14`, `--dry-run`, `--project X` are visible to
90
+ // the agent prompt). We only parse `--days` locally to short-circuit when the
91
+ // daily-log is genuinely empty.
93
92
  const argv = process.argv.slice(2);
94
93
  const flagIdx = argv.indexOf("--days");
95
94
  const days = flagIdx >= 0 ? parseInt(argv[flagIdx + 1], 10) || 7 : 7;
@@ -128,9 +127,21 @@ if (!dailyLogHasRecentEntries(days)) {
128
127
 
129
128
  // ── Run ──────────────────────────────────────────────────
130
129
  // `claude -p "<prompt>"` and `codex exec "<prompt>"` run a single
131
- // non-interactive turn. The skill body invocation matches what the user would
132
- // type at the prompt.
133
- const prompt = `/qualia-flush ${argv.join(" ")}`.trim();
130
+ // non-interactive turn. Keep the prompt self-contained so no separate flush
131
+ // slash command needs to be installed.
132
+ const argsText = argv.join(" ").trim() || "(none)";
133
+ const prompt = [
134
+ "Run the Qualia memory flush.",
135
+ "",
136
+ `Arguments: ${argsText}`,
137
+ `Install home: ${QUALIA_HOME}`,
138
+ "",
139
+ "Read recent markdown files from knowledge/daily-log under the install home.",
140
+ "Promote recurring patterns, decisions, fixes, and client preferences into the curated knowledge tier using bin/knowledge.js append.",
141
+ "Do not promote one-off noise. If --dry-run is present, report planned promotions without writing.",
142
+ "If --project NAME is present, limit promotions to that project.",
143
+ "Finish with one line starting exactly: ⬢ Flushed daily-log",
144
+ ].join("\n");
134
145
 
135
146
  const cliArgs = IS_CODEX_INSTALL ? ["exec", prompt] : ["-p", prompt];
136
147
  const result = spawnSync(agentBin, cliArgs, {
package/bin/knowledge.js CHANGED
@@ -79,7 +79,7 @@ function readSafe(p) {
79
79
  // 3. Path with "/" → treat as relative to knowledge dir (concepts/foo)
80
80
  // 4. Bare name → look in top-level first; if missing, search known
81
81
  // subdirectories (concepts/, daily-log/) for an exact match. This
82
- // means /qualia-flush can write to concepts/voice-agent-call-state.md
82
+ // means the memory flush can write to concepts/voice-agent-call-state.md
83
83
  // and skills can later run `knowledge.js load voice-agent-call-state`
84
84
  // without knowing it lives in a subdirectory.
85
85
  function resolveFile(name) {