qualia-framework 5.9.1 → 6.2.7

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 (81) hide show
  1. package/AGENTS.md +2 -1
  2. package/CLAUDE.md +2 -1
  3. package/README.md +45 -29
  4. package/agents/builder.md +1 -5
  5. package/agents/plan-checker.md +1 -1
  6. package/agents/planner.md +2 -6
  7. package/agents/qa-browser.md +3 -3
  8. package/agents/roadmapper.md +2 -2
  9. package/agents/verifier.md +7 -9
  10. package/agents/visual-evaluator.md +1 -3
  11. package/bin/cli.js +370 -205
  12. package/bin/erp-retry.js +11 -3
  13. package/bin/install.js +383 -55
  14. package/bin/knowledge-flush.js +25 -13
  15. package/bin/knowledge.js +11 -1
  16. package/bin/project-snapshot.js +293 -0
  17. package/bin/qualia-ui.js +13 -2
  18. package/bin/report-payload.js +137 -0
  19. package/bin/slop-detect.mjs +81 -9
  20. package/bin/state.js +8 -1
  21. package/bin/statusline.js +14 -2
  22. package/docs/archive/CHANGELOG-pre-v4.md +855 -0
  23. package/docs/changelog-v6.html +864 -0
  24. package/docs/ecosystem-operating-model.md +121 -0
  25. package/docs/erp-contract.md +74 -21
  26. package/docs/onboarding.html +2 -2
  27. package/docs/release.md +44 -0
  28. package/docs/reviews/v6.2.1-revival-audit.md +53 -0
  29. package/docs/reviews/v6.2.2-memory-erp-audit.md +41 -0
  30. package/docs/reviews/v6.2.3-erp-id-guard.md +15 -0
  31. package/guide.md +28 -3
  32. package/hooks/auto-update.js +20 -10
  33. package/hooks/branch-guard.js +10 -2
  34. package/hooks/env-empty-guard.js +15 -5
  35. package/hooks/git-guardrails.js +10 -1
  36. package/hooks/migration-guard.js +4 -1
  37. package/hooks/pre-deploy-gate.js +11 -1
  38. package/hooks/pre-push.js +43 -106
  39. package/hooks/session-start.js +22 -14
  40. package/hooks/stop-session-log.js +11 -3
  41. package/hooks/supabase-destructive-guard.js +11 -1
  42. package/hooks/vercel-account-guard.js +12 -3
  43. package/package.json +4 -3
  44. package/qualia-design/design-reference.md +2 -1
  45. package/qualia-design/frontend.md +4 -4
  46. package/rules/one-opinion.md +59 -0
  47. package/rules/trust-boundary.md +35 -0
  48. package/skills/qualia-feature/SKILL.md +5 -5
  49. package/skills/qualia-flush/SKILL.md +5 -7
  50. package/skills/qualia-hook-gen/SKILL.md +1 -1
  51. package/skills/qualia-learn/SKILL.md +1 -0
  52. package/skills/qualia-map/SKILL.md +2 -1
  53. package/skills/qualia-milestone/SKILL.md +2 -2
  54. package/skills/qualia-new/SKILL.md +6 -6
  55. package/skills/qualia-optimize/SKILL.md +1 -1
  56. package/skills/qualia-plan/SKILL.md +1 -1
  57. package/skills/qualia-polish/REFERENCE.md +8 -6
  58. package/skills/qualia-polish/SKILL.md +11 -9
  59. package/skills/qualia-polish/scripts/loop.mjs +18 -6
  60. package/skills/qualia-postmortem/SKILL.md +1 -1
  61. package/skills/qualia-report/SKILL.md +6 -42
  62. package/skills/qualia-road/SKILL.md +17 -5
  63. package/skills/qualia-verify/SKILL.md +3 -3
  64. package/skills/qualia-vibe/SKILL.md +226 -0
  65. package/skills/qualia-vibe/scripts/extract.mjs +141 -0
  66. package/skills/qualia-vibe/scripts/tokens.mjs +342 -0
  67. package/templates/help.html +10 -3
  68. package/templates/knowledge/agents.md +3 -3
  69. package/templates/knowledge/index.md +1 -1
  70. package/templates/tracking.json +3 -0
  71. package/templates/work-packet.md +46 -0
  72. package/tests/bin.test.sh +423 -25
  73. package/tests/hooks.test.sh +1 -8
  74. package/tests/install-smoke.test.sh +137 -0
  75. package/tests/published-install-smoke.test.sh +126 -0
  76. package/tests/refs.test.sh +43 -1
  77. package/tests/run-all.sh +49 -0
  78. package/tests/runner.js +19 -33
  79. package/tests/slop-detect.test.sh +11 -5
  80. package/tests/state.test.sh +4 -1
  81. package/hooks/pre-compact.js +0 -125
package/bin/cli.js CHANGED
@@ -4,6 +4,7 @@ const { spawnSync } = require("child_process");
4
4
  const path = require("path");
5
5
  const fs = require("fs");
6
6
  const readline = require("readline");
7
+ const os = require("os");
7
8
 
8
9
  const TEAL = "\x1b[38;2;0;206;209m";
9
10
  const TG = "\x1b[38;2;0;170;175m";
@@ -15,22 +16,77 @@ const RED = "\x1b[38;2;239;68;68m";
15
16
  const RESET = "\x1b[0m";
16
17
  const BOLD = "\x1b[1m";
17
18
 
18
- const CLAUDE_DIR = path.join(require("os").homedir(), ".claude");
19
+ const CLAUDE_DIR = path.join(os.homedir(), ".claude");
20
+ const CODEX_DIR = path.join(os.homedir(), ".codex");
19
21
  const PKG = require("../package.json");
20
22
  const CONFIG_FILE = path.join(CLAUDE_DIR, ".qualia-config.json");
23
+ const CODEX_CONFIG_FILE = path.join(CODEX_DIR, ".qualia-config.json");
24
+
25
+ function installedHomes() {
26
+ const homes = [];
27
+ if (fs.existsSync(CONFIG_FILE)) homes.push(CLAUDE_DIR);
28
+ if (fs.existsSync(CODEX_CONFIG_FILE)) homes.push(CODEX_DIR);
29
+ return homes.length > 0 ? homes : [CLAUDE_DIR];
30
+ }
31
+
32
+ function primaryInstallHome() {
33
+ return installedHomes()[0];
34
+ }
35
+
36
+ function configFileForHome(home) {
37
+ return path.join(home, ".qualia-config.json");
38
+ }
21
39
 
22
40
  function readConfig() {
41
+ const configFile = fs.existsSync(CONFIG_FILE) ? CONFIG_FILE : CODEX_CONFIG_FILE;
42
+ try {
43
+ return JSON.parse(fs.readFileSync(configFile, "utf8"));
44
+ } catch {
45
+ return {};
46
+ }
47
+ }
48
+
49
+ function readConfigAt(home) {
23
50
  try {
24
- return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
51
+ return JSON.parse(fs.readFileSync(configFileForHome(home), "utf8"));
25
52
  } catch {
26
53
  return {};
27
54
  }
28
55
  }
29
56
 
30
- function writeConfig(cfg) {
31
- if (!fs.existsSync(CLAUDE_DIR)) fs.mkdirSync(CLAUDE_DIR, { recursive: true });
32
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n", { mode: 0o600 });
33
- try { fs.chmodSync(CONFIG_FILE, 0o600); } catch {}
57
+ function writeConfig(cfg, home = primaryInstallHome()) {
58
+ if (!fs.existsSync(home)) fs.mkdirSync(home, { recursive: true });
59
+ const configFile = configFileForHome(home);
60
+ fs.writeFileSync(configFile, JSON.stringify(cfg, null, 2) + "\n", { mode: 0o600 });
61
+ try { fs.chmodSync(configFile, 0o600); } catch {}
62
+ }
63
+
64
+ function installTargetForUpdate() {
65
+ const hasClaude = fs.existsSync(CONFIG_FILE);
66
+ const hasCodex = fs.existsSync(CODEX_CONFIG_FILE);
67
+ if (hasClaude && hasCodex) return "3";
68
+ if (hasCodex) return "2";
69
+ return "1";
70
+ }
71
+
72
+ function isCodexHome(home) {
73
+ return path.basename(home) === ".codex";
74
+ }
75
+
76
+ function installLabel(home) {
77
+ return isCodexHome(home) ? "Codex" : "Claude Code";
78
+ }
79
+
80
+ function hasQualiaArtifacts(home) {
81
+ return fs.existsSync(configFileForHome(home))
82
+ || fs.existsSync(path.join(home, "bin", "state.js"))
83
+ || fs.existsSync(path.join(home, "hooks", "session-start.js"))
84
+ || fs.existsSync(path.join(home, "skills", "qualia-new"));
85
+ }
86
+
87
+ function frameworkHomes() {
88
+ const homes = [CLAUDE_DIR, CODEX_DIR].filter(hasQualiaArtifacts);
89
+ return homes.length > 0 ? homes : [CLAUDE_DIR];
34
90
  }
35
91
 
36
92
  function banner() {
@@ -50,6 +106,12 @@ function cmdVersion() {
50
106
  const cfg = readConfig();
51
107
 
52
108
  console.log(` ${DIM}Installed:${RESET} ${WHITE}${PKG.version}${RESET}`);
109
+ const surfaces = installedHomes()
110
+ .filter((home) => fs.existsSync(configFileForHome(home)))
111
+ .map((home) => path.basename(home) === ".codex" ? "Codex" : "Claude Code");
112
+ if (surfaces.length > 0) {
113
+ console.log(` ${DIM}Targets:${RESET} ${WHITE}${surfaces.join(" · ")}${RESET}`);
114
+ }
53
115
  if (cfg.installed_by) {
54
116
  console.log(` ${DIM}User:${RESET} ${WHITE}${cfg.installed_by}${RESET} ${DIM}(${cfg.role})${RESET}`);
55
117
  }
@@ -103,8 +165,9 @@ function cmdUpdate() {
103
165
  console.log("");
104
166
 
105
167
  try {
168
+ const target = installTargetForUpdate();
106
169
  const r = spawnSync("npx", ["qualia-framework@latest", "install"], {
107
- input: cfg.code + "\n",
170
+ input: `${cfg.code}\n${target}\n`,
108
171
  stdio: ["pipe", "inherit", "inherit"],
109
172
  shell: process.platform === "win32", // npx is a .cmd shim on Windows — must go through shell
110
173
  timeout: 120000,
@@ -122,12 +185,12 @@ function cmdUpdate() {
122
185
  }
123
186
 
124
187
  // ─── Uninstall ───────────────────────────────────────────
125
- // Surgical removal of the Qualia Framework from ~/.claude/.
126
- // Preserves CLAUDE.md (user may have customized it) and preserves any
127
- // non-Qualia entries in settings.json (other hooks, user env vars, etc.).
188
+ // Surgical removal of the Qualia Framework from installed homes.
189
+ // Preserves CLAUDE.md / AGENTS.md (user may have customized them), Codex
190
+ // config.toml, and non-Qualia hook entries in settings.json / hooks.json.
128
191
  // --yes / -y skips the confirmation prompt for scripted use.
129
192
 
130
- // Current Qualia hook filenames — only these are removed from ~/.claude/hooks/,
193
+ // Current Qualia hook filenames — only these are removed from hooks/,
131
194
  // any other hooks the user dropped in there are left alone. The LEGACY set
132
195
  // lists hooks that were shipped by older framework versions but have since
133
196
  // been removed; uninstall still tries to clean them so old installs get a
@@ -139,15 +202,18 @@ const QUALIA_HOOK_FILES = [
139
202
  "pre-push.js",
140
203
  "migration-guard.js",
141
204
  "pre-deploy-gate.js",
142
- "pre-compact.js",
143
205
  "git-guardrails.js",
144
206
  "stop-session-log.js",
207
+ "env-empty-guard.js",
208
+ "supabase-destructive-guard.js",
209
+ "vercel-account-guard.js",
145
210
  ];
146
211
  const QUALIA_LEGACY_HOOK_FILES = [
147
212
  "block-env-edit.js", // removed in v3.2.0
213
+ "pre-compact.js", // removed in v6.2.0 — state.js journal makes bot-commits redundant
148
214
  ];
149
215
 
150
- // 8 Qualia agents — only these are removed.
216
+ // Qualia agents — only these are removed.
151
217
  const QUALIA_AGENT_FILES = [
152
218
  "planner.md",
153
219
  "builder.md",
@@ -157,15 +223,30 @@ const QUALIA_AGENT_FILES = [
157
223
  "researcher.md",
158
224
  "research-synthesizer.md",
159
225
  "roadmapper.md",
226
+ "visual-evaluator.md",
227
+ ];
228
+ const QUALIA_CODEX_AGENT_FILES = QUALIA_AGENT_FILES.map((f) => f.replace(/\.md$/, ".toml"));
229
+
230
+ // Qualia bin scripts.
231
+ const QUALIA_BIN_FILES = [
232
+ "state.js",
233
+ "qualia-ui.js",
234
+ "statusline.js",
235
+ "knowledge.js",
236
+ "knowledge-flush.js",
237
+ "plan-contract.js",
238
+ "agent-runs.js",
239
+ "slop-detect.mjs",
240
+ "erp-retry.js",
241
+ "report-payload.js",
242
+ "project-snapshot.js",
160
243
  ];
161
-
162
- // 3 Qualia bin scripts.
163
- const QUALIA_BIN_FILES = ["state.js", "qualia-ui.js", "statusline.js", "knowledge.js", "knowledge-flush.js", "plan-contract.js", "agent-runs.js", "slop-detect.mjs", "erp-retry.js"];
164
244
 
165
245
  // Qualia rules — security, deployment, infra, grounding, plus the v4.5.0 design substrate.
166
246
  // frontend.md and design-reference.md are kept for backward compat; new projects use design-laws/brand/product/rubric.
167
247
  const QUALIA_RULE_FILES = [
168
248
  "security.md", "deployment.md", "infrastructure.md", "grounding.md",
249
+ "speed.md", "architecture.md", "trust-boundary.md", "one-opinion.md",
169
250
  "frontend.md", "design-reference.md",
170
251
  "design-laws.md", "design-brand.md", "design-product.md", "design-rubric.md",
171
252
  ];
@@ -205,46 +286,55 @@ function safeRmDir(p, counters) {
205
286
  }
206
287
  }
207
288
 
208
- function cleanSettingsJson(counters) {
209
- const settingsPath = path.join(CLAUDE_DIR, "settings.json");
210
- if (!fs.existsSync(settingsPath)) return;
211
- let settings;
212
- try {
213
- settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
214
- } catch (e) {
215
- counters.errors.push(`settings.json: ${e.message}`);
216
- return;
217
- }
218
-
219
- // Only remove entries that point at qualia paths. Leave everything else.
220
- const isQualiaCommand = (cmd) =>
221
- typeof cmd === "string" && (cmd.includes("qualia") || cmd.includes(".claude/hooks/") || cmd.includes(".claude/bin/"));
289
+ function isQualiaCommand(cmd) {
290
+ return typeof cmd === "string"
291
+ && (cmd.includes("qualia")
292
+ || cmd.includes(".claude/hooks/")
293
+ || cmd.includes(".claude/bin/")
294
+ || cmd.includes(".codex/hooks/")
295
+ || cmd.includes(".codex/bin/"));
296
+ }
222
297
 
223
- const filterHookArray = (arr) => {
224
- if (!Array.isArray(arr)) return arr;
225
- return arr
298
+ function cleanHookBlocks(root, hookKey) {
299
+ if (!root || typeof root !== "object" || !root[hookKey] || typeof root[hookKey] !== "object") return false;
300
+ let changed = false;
301
+ for (const key of Object.keys(root[hookKey])) {
302
+ const arr = root[hookKey][key];
303
+ if (!Array.isArray(arr)) continue;
304
+ const cleaned = arr
226
305
  .map((entry) => {
227
306
  if (!entry || !Array.isArray(entry.hooks)) return entry;
228
307
  const hooks = entry.hooks.filter((h) => !isQualiaCommand(h && h.command));
308
+ if (hooks.length !== entry.hooks.length) changed = true;
229
309
  return { ...entry, hooks };
230
310
  })
231
311
  .filter((entry) => Array.isArray(entry.hooks) && entry.hooks.length > 0);
232
- };
233
-
234
- if (settings.hooks && typeof settings.hooks === "object") {
235
- // Iterate every hook event key, not a hardcoded subset — future hook
236
- // events added by Claude Code or the framework get cleaned automatically.
237
- for (const key of Object.keys(settings.hooks)) {
238
- const cleaned = filterHookArray(settings.hooks[key]);
239
- if (cleaned && cleaned.length > 0) {
240
- settings.hooks[key] = cleaned;
241
- } else {
242
- delete settings.hooks[key];
243
- }
312
+ if (cleaned.length > 0) {
313
+ root[hookKey][key] = cleaned;
314
+ } else {
315
+ delete root[hookKey][key];
316
+ changed = true;
244
317
  }
245
- // If hooks is now empty, remove it entirely.
246
- if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
247
318
  }
319
+ if (Object.keys(root[hookKey]).length === 0) {
320
+ delete root[hookKey];
321
+ changed = true;
322
+ }
323
+ return changed;
324
+ }
325
+
326
+ function cleanSettingsJson(home, counters) {
327
+ const settingsPath = path.join(home, "settings.json");
328
+ if (!fs.existsSync(settingsPath)) return;
329
+ let settings;
330
+ try {
331
+ settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
332
+ } catch (e) {
333
+ counters.errors.push(`settings.json: ${e.message}`);
334
+ return;
335
+ }
336
+
337
+ cleanHookBlocks(settings, "hooks");
248
338
 
249
339
  // Status line — only drop it if it points at our renderer.
250
340
  if (settings.statusLine && typeof settings.statusLine === "object") {
@@ -268,19 +358,40 @@ function cleanSettingsJson(counters) {
268
358
  }
269
359
  }
270
360
 
361
+ function cleanCodexHooksJson(home, counters) {
362
+ const hooksPath = path.join(home, "hooks.json");
363
+ if (!fs.existsSync(hooksPath)) return;
364
+ let hooksJson;
365
+ try {
366
+ hooksJson = JSON.parse(fs.readFileSync(hooksPath, "utf8"));
367
+ } catch (e) {
368
+ counters.errors.push(`hooks.json: ${e.message}`);
369
+ return;
370
+ }
371
+ cleanHookBlocks(hooksJson, "hooks");
372
+ try {
373
+ fs.writeFileSync(hooksPath, JSON.stringify(hooksJson, null, 2) + "\n");
374
+ counters.hooksJsonCleaned = true;
375
+ } catch (e) {
376
+ counters.errors.push(`hooks.json write: ${e.message}`);
377
+ }
378
+ }
379
+
271
380
  async function cmdUninstall() {
272
381
  banner();
273
382
 
274
383
  const args = process.argv.slice(3);
275
384
  const skipConfirm = args.includes("-y") || args.includes("--yes");
276
385
 
386
+ const homes = frameworkHomes();
277
387
  const cfg = readConfig();
278
388
  console.log("");
279
389
  if (cfg.installed_by) {
280
390
  console.log(` ${DIM}User:${RESET} ${WHITE}${cfg.installed_by}${RESET} ${DIM}(${cfg.role || "?"})${RESET}`);
281
391
  } else {
282
- console.log(` ${DIM}No Qualia config found at${RESET} ${WHITE}${CONFIG_FILE}${RESET}`);
392
+ console.log(` ${DIM}No Qualia config found at${RESET} ${WHITE}${CONFIG_FILE}${RESET} ${DIM}or${RESET} ${WHITE}${CODEX_CONFIG_FILE}${RESET}`);
283
393
  }
394
+ console.log(` ${DIM}Targets:${RESET} ${WHITE}${homes.map(installLabel).join(" · ")}${RESET}`);
284
395
  console.log("");
285
396
 
286
397
  if (!skipConfirm) {
@@ -306,62 +417,60 @@ async function cmdUninstall() {
306
417
  console.log(` ${DIM}Removing framework files...${RESET}`);
307
418
  console.log("");
308
419
 
309
- const counters = { filesRemoved: 0, dirsRemoved: 0, settingsCleaned: false, errors: [] };
420
+ const counters = { filesRemoved: 0, dirsRemoved: 0, settingsCleaned: false, hooksJsonCleaned: false, errors: [] };
310
421
 
311
- // Skills any directory starting with "qualia" under ~/.claude/skills/.
312
- const skillsDir = path.join(CLAUDE_DIR, "skills");
313
- try {
314
- if (fs.existsSync(skillsDir)) {
315
- for (const name of fs.readdirSync(skillsDir)) {
316
- if (name === "qualia" || name.startsWith("qualia-")) {
317
- safeRmDir(path.join(skillsDir, name), counters);
422
+ for (const home of homes) {
423
+ const skillsDir = path.join(home, "skills");
424
+ try {
425
+ if (fs.existsSync(skillsDir)) {
426
+ for (const name of fs.readdirSync(skillsDir)) {
427
+ if (name === "qualia" || name.startsWith("qualia-")) {
428
+ safeRmDir(path.join(skillsDir, name), counters);
429
+ }
318
430
  }
319
431
  }
432
+ } catch (e) {
433
+ counters.errors.push(`${installLabel(home)} skills scan: ${e.message}`);
320
434
  }
321
- } catch (e) {
322
- counters.errors.push(`skills scan: ${e.message}`);
323
- }
324
435
 
325
- // Agents only the 4 Qualia ones.
326
- for (const f of QUALIA_AGENT_FILES) {
327
- safeUnlink(path.join(CLAUDE_DIR, "agents", f), counters);
328
- }
436
+ const agentFiles = isCodexHome(home)
437
+ ? [...QUALIA_CODEX_AGENT_FILES, ...QUALIA_AGENT_FILES]
438
+ : QUALIA_AGENT_FILES;
439
+ for (const f of agentFiles) {
440
+ safeUnlink(path.join(home, "agents", f), counters);
441
+ }
329
442
 
330
- // Hooks current set plus any legacy hook filenames from older versions.
331
- for (const f of [...QUALIA_HOOK_FILES, ...QUALIA_LEGACY_HOOK_FILES]) {
332
- safeUnlink(path.join(CLAUDE_DIR, "hooks", f), counters);
333
- }
443
+ for (const f of [...QUALIA_HOOK_FILES, ...QUALIA_LEGACY_HOOK_FILES]) {
444
+ safeUnlink(path.join(home, "hooks", f), counters);
445
+ }
334
446
 
335
- // Bin scripts only the 3 Qualia ones.
336
- for (const f of QUALIA_BIN_FILES) {
337
- safeUnlink(path.join(CLAUDE_DIR, "bin", f), counters);
338
- }
447
+ for (const f of QUALIA_BIN_FILES) {
448
+ safeUnlink(path.join(home, "bin", f), counters);
449
+ }
339
450
 
340
- // Rules all 4.
341
- for (const f of QUALIA_RULE_FILES) {
342
- safeUnlink(path.join(CLAUDE_DIR, "rules", f), counters);
343
- }
451
+ for (const f of QUALIA_RULE_FILES) {
452
+ safeUnlink(path.join(home, "rules", f), counters);
453
+ }
344
454
 
345
- // Templates directory (entire).
346
- safeRmDir(path.join(CLAUDE_DIR, "qualia-templates"), counters);
455
+ safeRmDir(path.join(home, "qualia-templates"), counters);
456
+ safeRmDir(path.join(home, "qualia-design"), counters);
457
+ safeRmDir(path.join(home, "qualia-references"), counters);
347
458
 
348
- // Knowledge directory (optional preservation).
349
- if (!preserveKnowledge) {
350
- safeRmDir(path.join(CLAUDE_DIR, "knowledge"), counters);
351
- }
459
+ if (!preserveKnowledge) {
460
+ safeRmDir(path.join(home, "knowledge"), counters);
461
+ }
352
462
 
353
- // Config + state files.
354
- safeUnlink(path.join(CLAUDE_DIR, ".qualia-config.json"), counters);
355
- safeUnlink(path.join(CLAUDE_DIR, ".qualia-last-update-check"), counters);
356
- safeUnlink(path.join(CLAUDE_DIR, ".erp-api-key"), counters);
357
- safeUnlink(path.join(CLAUDE_DIR, ".qualia-team.json"), counters);
358
- safeUnlink(path.join(CLAUDE_DIR, "qualia-guide.md"), counters);
463
+ safeUnlink(path.join(home, ".qualia-config.json"), counters);
464
+ safeUnlink(path.join(home, ".qualia-last-update-check"), counters);
465
+ safeUnlink(path.join(home, ".erp-api-key"), counters);
466
+ safeUnlink(path.join(home, ".qualia-team.json"), counters);
467
+ safeUnlink(path.join(home, "qualia-guide.md"), counters);
359
468
 
360
- // Traces directory.
361
- safeRmDir(path.join(CLAUDE_DIR, ".qualia-traces"), counters);
469
+ safeRmDir(path.join(home, ".qualia-traces"), counters);
362
470
 
363
- // Clean settings.json surgically.
364
- cleanSettingsJson(counters);
471
+ if (isCodexHome(home)) cleanCodexHooksJson(home, counters);
472
+ else cleanSettingsJson(home, counters);
473
+ }
365
474
 
366
475
  // Summary.
367
476
  console.log("");
@@ -372,6 +481,9 @@ async function cmdUninstall() {
372
481
  console.log(
373
482
  ` ${DIM}settings.json:${RESET} ${counters.settingsCleaned ? `${GREEN}cleaned ✓${RESET}` : `${DIM}not present${RESET}`}`
374
483
  );
484
+ console.log(
485
+ ` ${DIM}hooks.json:${RESET} ${counters.hooksJsonCleaned ? `${GREEN}cleaned ✓${RESET}` : `${DIM}not present${RESET}`}`
486
+ );
375
487
  if (preserveKnowledge) {
376
488
  console.log(` ${DIM}Knowledge base:${RESET} ${GREEN}preserved ✓${RESET}`);
377
489
  } else {
@@ -387,14 +499,13 @@ async function cmdUninstall() {
387
499
  }
388
500
 
389
501
  console.log("");
390
- console.log(
391
- ` ${YELLOW}Manual step:${RESET} edit ${WHITE}~/.claude/CLAUDE.md${RESET} to remove the Qualia Framework section if desired.`
392
- );
502
+ console.log(` ${YELLOW}Manual step:${RESET} edit ${WHITE}~/.claude/CLAUDE.md${RESET} or ${WHITE}~/.codex/AGENTS.md${RESET} if you want to remove the global instruction text.`);
393
503
  console.log("");
394
504
  }
395
505
 
396
506
  // ─── Team Management ────────────────────────────────────
397
- // External team file at ~/.claude/.qualia-team.json.
507
+ // External team file at the active install home. Falls back to ~/.claude before
508
+ // install for backward compatibility.
398
509
  // Falls back to embedded defaults in install.js.
399
510
 
400
511
  function getDefaultTeam() {
@@ -408,19 +519,23 @@ function getDefaultTeam() {
408
519
  }
409
520
 
410
521
  function readTeamFile() {
411
- const teamFile = path.join(CLAUDE_DIR, ".qualia-team.json");
412
- try {
413
- if (fs.existsSync(teamFile)) {
414
- const data = JSON.parse(fs.readFileSync(teamFile, "utf8"));
415
- if (data && typeof data === "object" && Object.keys(data).length > 0) return data;
416
- }
417
- } catch {}
522
+ for (const home of installedHomes()) {
523
+ const teamFile = path.join(home, ".qualia-team.json");
524
+ try {
525
+ if (fs.existsSync(teamFile)) {
526
+ const data = JSON.parse(fs.readFileSync(teamFile, "utf8"));
527
+ if (data && typeof data === "object" && Object.keys(data).length > 0) return data;
528
+ }
529
+ } catch {}
530
+ }
418
531
  return null;
419
532
  }
420
533
 
421
534
  function writeTeamFile(team) {
422
- if (!fs.existsSync(CLAUDE_DIR)) fs.mkdirSync(CLAUDE_DIR, { recursive: true });
423
- fs.writeFileSync(path.join(CLAUDE_DIR, ".qualia-team.json"), JSON.stringify(team, null, 2) + "\n");
535
+ for (const home of installedHomes()) {
536
+ if (!fs.existsSync(home)) fs.mkdirSync(home, { recursive: true });
537
+ fs.writeFileSync(path.join(home, ".qualia-team.json"), JSON.stringify(team, null, 2) + "\n");
538
+ }
424
539
  }
425
540
 
426
541
  function parseTeamArgs(argv) {
@@ -442,7 +557,7 @@ function cmdTeam() {
442
557
  console.log("");
443
558
  const team = readTeamFile();
444
559
  const source = team || getDefaultTeam();
445
- const label = team ? "team file" : "embedded defaults";
560
+ const label = team ? "installed team file" : "embedded defaults";
446
561
  console.log(` ${DIM}Source: ${label}${RESET}`);
447
562
  console.log("");
448
563
  for (const [code, member] of Object.entries(source)) {
@@ -502,7 +617,7 @@ function cmdTeam() {
502
617
  function cmdTraces() {
503
618
  banner();
504
619
  console.log("");
505
- const tracesDir = path.join(CLAUDE_DIR, ".qualia-traces");
620
+ const tracesDir = path.join(primaryInstallHome(), ".qualia-traces");
506
621
  if (!fs.existsSync(tracesDir)) {
507
622
  console.log(` ${DIM}No traces found. Traces are written by hooks during normal operation.${RESET}`);
508
623
  console.log("");
@@ -538,6 +653,15 @@ function cmdMigrate() {
538
653
 
539
654
  const settingsPath = path.join(CLAUDE_DIR, "settings.json");
540
655
  if (!fs.existsSync(settingsPath)) {
656
+ if (fs.existsSync(CODEX_CONFIG_FILE)) {
657
+ const hooksPath = path.join(CODEX_DIR, "hooks.json");
658
+ if (fs.existsSync(hooksPath)) {
659
+ console.log(` ${GREEN}✓${RESET} Codex install uses ${WHITE}~/.codex/hooks.json${RESET}; no Claude settings migration is needed.`);
660
+ console.log(` ${DIM}Refresh Codex wiring with:${RESET} ${TEAL}npx qualia-framework@latest install${RESET}`);
661
+ console.log("");
662
+ process.exit(0);
663
+ }
664
+ }
541
665
  console.log(` ${RED}✗${RESET} No settings.json found. Run ${TEAL}qualia-framework install${RESET} first.`);
542
666
  console.log("");
543
667
  process.exit(1);
@@ -582,7 +706,16 @@ function cmdMigrate() {
582
706
  }
583
707
 
584
708
  // Check PreToolUse hooks — ensure all critical hooks are present
585
- const requiredBashHooks = ["auto-update.js", "git-guardrails.js", "branch-guard.js", "pre-push.js", "pre-deploy-gate.js"];
709
+ const requiredBashHooks = [
710
+ "auto-update.js",
711
+ "git-guardrails.js",
712
+ "branch-guard.js",
713
+ "pre-push.js",
714
+ "pre-deploy-gate.js",
715
+ "vercel-account-guard.js",
716
+ "env-empty-guard.js",
717
+ "supabase-destructive-guard.js",
718
+ ];
586
719
  const requiredEditHooks = ["migration-guard.js"];
587
720
 
588
721
  if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
@@ -614,6 +747,9 @@ function cmdMigrate() {
614
747
  if (hookFile === "branch-guard.js") { hookDef.if = "Bash(git push*)"; hookDef.statusMessage = "⬢ Checking branch permissions..."; }
615
748
  if (hookFile === "pre-push.js") { hookDef.if = "Bash(git push*)"; hookDef.timeout = 15; }
616
749
  if (hookFile === "pre-deploy-gate.js") { hookDef.if = "Bash(vercel --prod*)"; hookDef.timeout = 180; hookDef.statusMessage = "⬢ Running quality gates..."; }
750
+ if (hookFile === "vercel-account-guard.js") { hookDef.if = "Bash(vercel --prod*)|Bash(vercel deploy*)"; hookDef.timeout = 8; hookDef.statusMessage = "⬢ Verifying Vercel account..."; }
751
+ if (hookFile === "env-empty-guard.js") { hookDef.if = "Bash(vercel env*)"; hookDef.statusMessage = "⬢ Checking env value..."; }
752
+ if (hookFile === "supabase-destructive-guard.js") { hookDef.if = "Bash(supabase*)|Bash(npx supabase*)"; hookDef.statusMessage = "⬢ Checking Supabase safety..."; }
617
753
  bashEntry.hooks.push(hookDef);
618
754
  changes++;
619
755
  console.log(` ${GREEN}+${RESET} Wired ${hookFile} into PreToolUse/Bash`);
@@ -641,23 +777,22 @@ function cmdMigrate() {
641
777
  }
642
778
  }
643
779
 
644
- // Check PreCompact hook
645
- if (!settings.hooks.PreCompact || !Array.isArray(settings.hooks.PreCompact)) {
646
- settings.hooks.PreCompact = [{ matcher: "compact", hooks: [{ type: "command", command: nodeCmd("pre-compact.js"), timeout: 15 }] }];
647
- changes++;
648
- console.log(` ${GREEN}+${RESET} Added PreCompact hook`);
649
- } else {
650
- let compactEntry = settings.hooks.PreCompact.find(e => e.matcher === "compact");
651
- if (!compactEntry) {
652
- compactEntry = { matcher: "compact", hooks: [] };
653
- settings.hooks.PreCompact.push(compactEntry);
654
- }
655
- if (!compactEntry.hooks) compactEntry.hooks = [];
656
- const exists = compactEntry.hooks.some(h => extractScriptName(h.command) === "pre-compact.js");
657
- if (!exists) {
658
- compactEntry.hooks.push({ type: "command", command: nodeCmd("pre-compact.js"), timeout: 15, statusMessage: "⬢ Saving state..." });
780
+ // PreCompact: pre-compact.js was removed in v6.2.0 (state.js already provides
781
+ // crash-safe atomic writes with a write-ahead journal — the bot commit added
782
+ // no durability). Strip any legacy entry; drop the event key if it's empty.
783
+ if (Array.isArray(settings.hooks.PreCompact)) {
784
+ const beforeLen = settings.hooks.PreCompact.length;
785
+ settings.hooks.PreCompact = settings.hooks.PreCompact
786
+ .map(block => {
787
+ if (!block || !Array.isArray(block.hooks)) return block;
788
+ return { ...block, hooks: block.hooks.filter(h => extractScriptName(h && h.command) !== "pre-compact.js") };
789
+ })
790
+ .filter(block => Array.isArray(block.hooks) && block.hooks.length > 0);
791
+ const removed = beforeLen !== settings.hooks.PreCompact.length || settings.hooks.PreCompact.length === 0;
792
+ if (settings.hooks.PreCompact.length === 0) delete settings.hooks.PreCompact;
793
+ if (removed) {
659
794
  changes++;
660
- console.log(` ${GREEN}+${RESET} Wired pre-compact.js into PreCompact`);
795
+ console.log(` ${GREEN}-${RESET} Removed legacy pre-compact.js from PreCompact`);
661
796
  }
662
797
  }
663
798
 
@@ -738,7 +873,7 @@ function cmdAnalytics() {
738
873
  banner();
739
874
  console.log("");
740
875
 
741
- const tracesDir = path.join(CLAUDE_DIR, ".qualia-traces");
876
+ const tracesDir = path.join(primaryInstallHome(), ".qualia-traces");
742
877
  if (!fs.existsSync(tracesDir)) {
743
878
  console.log(` ${DIM}No traces found. Analytics require hook telemetry data.${RESET}`);
744
879
  console.log(` ${DIM}Traces are collected automatically during normal framework use.${RESET}`);
@@ -828,11 +963,12 @@ async function cmdErpPing() {
828
963
  banner();
829
964
  console.log("");
830
965
 
966
+ const installHome = primaryInstallHome();
831
967
  const cfg = readConfig();
832
968
  const args = new Set(process.argv.slice(3));
833
969
  const erpUrl = (cfg.erp && cfg.erp.url) || "https://portal.qualiasolutions.net";
834
970
  let erpEnabled = !(cfg.erp && cfg.erp.enabled === false);
835
- const keyFile = path.join(CLAUDE_DIR, ".erp-api-key");
971
+ const keyFile = path.join(installHome, ".erp-api-key");
836
972
 
837
973
  console.log(` ${DIM}URL:${RESET} ${WHITE}${erpUrl}${RESET}`);
838
974
  console.log(` ${DIM}Enabled:${RESET} ${erpEnabled ? `${GREEN}yes${RESET}` : `${YELLOW}no (erp.enabled=false)${RESET}`}`);
@@ -963,17 +1099,21 @@ function cmdSetErpKey() {
963
1099
  banner();
964
1100
  console.log("");
965
1101
 
966
- const keyFile = path.join(CLAUDE_DIR, ".erp-api-key");
1102
+ const homes = installedHomes();
967
1103
  const rawArgs = process.argv.slice(3);
968
1104
  const clear = rawArgs.includes("--clear");
969
1105
 
970
- if (!fs.existsSync(CLAUDE_DIR)) fs.mkdirSync(CLAUDE_DIR, { recursive: true });
1106
+ for (const home of homes) {
1107
+ if (!fs.existsSync(home)) fs.mkdirSync(home, { recursive: true });
1108
+ }
971
1109
 
972
1110
  if (clear) {
973
- try { fs.unlinkSync(keyFile); } catch {}
974
- const cfg = readConfig();
975
- cfg.erp = { ...(cfg.erp || {}), enabled: false, url: (cfg.erp && cfg.erp.url) || "https://portal.qualiasolutions.net", api_key_file: ".erp-api-key" };
976
- writeConfig(cfg);
1111
+ for (const home of homes) {
1112
+ try { fs.unlinkSync(path.join(home, ".erp-api-key")); } catch {}
1113
+ const cfg = readConfigAt(home);
1114
+ cfg.erp = { ...(cfg.erp || {}), enabled: false, url: (cfg.erp && cfg.erp.url) || "https://portal.qualiasolutions.net", api_key_file: ".erp-api-key" };
1115
+ writeConfig(cfg, home);
1116
+ }
977
1117
  console.log(` ${GREEN}✓${RESET} ERP key removed and ERP disabled.`);
978
1118
  console.log("");
979
1119
  return;
@@ -1010,19 +1150,21 @@ function cmdSetErpKey() {
1010
1150
  console.log(` ${YELLOW}!${RESET} Key looks short (${key.length} bytes). Saving anyway.`);
1011
1151
  }
1012
1152
 
1013
- fs.writeFileSync(keyFile, key, { mode: 0o600 });
1014
- try { fs.chmodSync(keyFile, 0o600); } catch {}
1015
-
1016
- const cfg = readConfig();
1017
- cfg.erp = {
1018
- ...(cfg.erp || {}),
1019
- enabled: true,
1020
- url: (cfg.erp && cfg.erp.url) || "https://portal.qualiasolutions.net",
1021
- api_key_file: ".erp-api-key",
1022
- };
1023
- writeConfig(cfg);
1153
+ for (const home of homes) {
1154
+ const keyFile = path.join(home, ".erp-api-key");
1155
+ fs.writeFileSync(keyFile, key, { mode: 0o600 });
1156
+ try { fs.chmodSync(keyFile, 0o600); } catch {}
1157
+ const cfg = readConfigAt(home);
1158
+ cfg.erp = {
1159
+ ...(cfg.erp || {}),
1160
+ enabled: true,
1161
+ url: (cfg.erp && cfg.erp.url) || "https://portal.qualiasolutions.net",
1162
+ api_key_file: ".erp-api-key",
1163
+ };
1164
+ writeConfig(cfg, home);
1165
+ }
1024
1166
 
1025
- console.log(` ${GREEN}✓${RESET} ERP key saved to ${WHITE}${keyFile}${RESET}`);
1167
+ console.log(` ${GREEN}✓${RESET} ERP key saved to ${WHITE}${homes.map((h) => path.join(h, ".erp-api-key")).join(", ")}${RESET}`);
1026
1168
  console.log(` ${DIM}Verify with:${RESET} ${TEAL}qualia-framework erp-ping${RESET}`);
1027
1169
  console.log("");
1028
1170
  }
@@ -1040,7 +1182,7 @@ function cmdSetErpKey() {
1040
1182
  // retry stranded reports on demand (e.g., after the ERP came back online,
1041
1183
  // or after rotating the API key). All args pass through.
1042
1184
  function cmdErpFlush() {
1043
- const retryScript = path.join(CLAUDE_DIR, "bin", "erp-retry.js");
1185
+ const retryScript = path.join(primaryInstallHome(), "bin", "erp-retry.js");
1044
1186
  if (!fs.existsSync(retryScript)) {
1045
1187
  console.log(` ${RED}✗${RESET} erp-retry.js not installed at ${retryScript}`);
1046
1188
  console.log(` ${DIM}Run: npx qualia-framework@latest install${RESET}`);
@@ -1058,8 +1200,25 @@ function cmdErpFlush() {
1058
1200
  process.exit(r.status || 0);
1059
1201
  }
1060
1202
 
1203
+ function cmdProjectSnapshot() {
1204
+ const snapshotScript = path.join(primaryInstallHome(), "bin", "project-snapshot.js");
1205
+ const localSnapshotScript = path.join(__dirname, "project-snapshot.js");
1206
+ const script = fs.existsSync(snapshotScript) ? snapshotScript : localSnapshotScript;
1207
+ if (!fs.existsSync(script)) {
1208
+ console.log(` ${RED}✗${RESET} project-snapshot.js not available`);
1209
+ console.log(` ${DIM}Run: npx qualia-framework@latest install${RESET}`);
1210
+ process.exit(1);
1211
+ }
1212
+ const args = process.argv.slice(3);
1213
+ const r = spawnSync(process.execPath, [script, ...args], {
1214
+ stdio: "inherit",
1215
+ shell: false,
1216
+ });
1217
+ process.exit(typeof r.status === "number" ? r.status : 1);
1218
+ }
1219
+
1061
1220
  function cmdFlush() {
1062
- const flushScript = path.join(CLAUDE_DIR, "bin", "knowledge-flush.js");
1221
+ const flushScript = path.join(primaryInstallHome(), "bin", "knowledge-flush.js");
1063
1222
  if (!fs.existsSync(flushScript)) {
1064
1223
  console.log(` ${RED}✗${RESET} knowledge-flush.js not installed at ${flushScript}`);
1065
1224
  console.log(` ${DIM}Run: npx qualia-framework@latest install${RESET}`);
@@ -1085,70 +1244,71 @@ function cmdDoctor() {
1085
1244
  if (!ok) issues.push({ label, hint });
1086
1245
  }
1087
1246
 
1088
- // ── Critical files (the same set session-start.js validates) ──
1089
- const criticalFiles = [
1090
- path.join(CLAUDE_DIR, "rules", "grounding.md"),
1091
- path.join(CLAUDE_DIR, "rules", "security.md"),
1092
- path.join(CLAUDE_DIR, "rules", "frontend.md"),
1093
- path.join(CLAUDE_DIR, "rules", "deployment.md"),
1094
- path.join(CLAUDE_DIR, "bin", "state.js"),
1095
- path.join(CLAUDE_DIR, "bin", "qualia-ui.js"),
1096
- path.join(CLAUDE_DIR, "bin", "statusline.js"),
1097
- path.join(CLAUDE_DIR, "bin", "knowledge.js"),
1098
- path.join(CLAUDE_DIR, "bin", "knowledge-flush.js"),
1099
- path.join(CLAUDE_DIR, "bin", "erp-retry.js"),
1100
- path.join(CLAUDE_DIR, "CLAUDE.md"),
1101
- CONFIG_FILE,
1102
- ];
1103
- for (const f of criticalFiles) {
1104
- check(
1105
- `${path.relative(CLAUDE_DIR, f) || f}`,
1106
- fs.existsSync(f),
1107
- "run: npx qualia-framework@latest install",
1108
- );
1109
- }
1247
+ const homes = installedHomes().filter((home) => fs.existsSync(configFileForHome(home)));
1248
+ if (homes.length === 0) {
1249
+ check("Qualia install", false, "run: npx qualia-framework@latest install");
1250
+ }
1251
+
1252
+ for (const home of homes) {
1253
+ const label = path.basename(home) === ".codex" ? "Codex" : "Claude";
1254
+ const coreFiles = [
1255
+ "rules/grounding.md",
1256
+ "rules/security.md",
1257
+ "rules/deployment.md",
1258
+ "bin/state.js",
1259
+ "bin/qualia-ui.js",
1260
+ "bin/statusline.js",
1261
+ "bin/knowledge.js",
1262
+ "bin/knowledge-flush.js",
1263
+ "bin/erp-retry.js",
1264
+ "bin/report-payload.js",
1265
+ "bin/project-snapshot.js",
1266
+ "knowledge/agents.md",
1267
+ "knowledge/index.md",
1268
+ "knowledge/daily-log",
1269
+ ".qualia-config.json",
1270
+ ];
1271
+ if (label === "Claude") coreFiles.push("CLAUDE.md", "settings.json");
1272
+ else coreFiles.push("AGENTS.md", "config.toml", "hooks.json", "agents/planner.toml", "skills/qualia-new/SKILL.md");
1273
+
1274
+ for (const rel of coreFiles) {
1275
+ check(`${label} ${rel}`, fs.existsSync(path.join(home, rel)), "run: npx qualia-framework@latest install");
1276
+ }
1110
1277
 
1111
- // ── Hooks ─────────────────────────────────────────────
1112
- for (const h of QUALIA_HOOK_FILES) {
1113
- check(
1114
- `hooks/${h}`,
1115
- fs.existsSync(path.join(CLAUDE_DIR, "hooks", h)),
1116
- "reinstall: npx qualia-framework@latest install",
1117
- );
1118
- }
1278
+ for (const h of QUALIA_HOOK_FILES) {
1279
+ check(`${label} hooks/${h}`, fs.existsSync(path.join(home, "hooks", h)), "reinstall: npx qualia-framework@latest install");
1280
+ }
1119
1281
 
1120
- // ── Knowledge layer ────────────────────────────────────
1121
- const knowledgeFiles = [
1122
- path.join(CLAUDE_DIR, "knowledge", "agents.md"),
1123
- path.join(CLAUDE_DIR, "knowledge", "index.md"),
1124
- path.join(CLAUDE_DIR, "knowledge", "daily-log"),
1125
- ];
1126
- for (const f of knowledgeFiles) {
1127
- check(
1128
- `knowledge/${path.basename(f)}${fs.existsSync(f) && fs.statSync(f).isDirectory() ? "/" : ""}`,
1129
- fs.existsSync(f),
1130
- "reinstall to initialize the memory layer: npx qualia-framework@latest install",
1131
- );
1132
- }
1282
+ if (label === "Claude" && fs.existsSync(path.join(home, "settings.json"))) {
1283
+ try {
1284
+ const settings = JSON.parse(fs.readFileSync(path.join(home, "settings.json"), "utf8"));
1285
+ for (const ev of ["SessionStart", "PreToolUse", "Stop"]) {
1286
+ const blocks = (settings.hooks || {})[ev] || [];
1287
+ const hasQualia = blocks.some((b) =>
1288
+ (b.hooks || []).some((h) => typeof h.command === "string" && h.command.includes(".claude")),
1289
+ );
1290
+ check(`Claude settings.json hooks.${ev}`, hasQualia, "reinstall to wire hooks");
1291
+ }
1292
+ check("Claude settings.json statusLine", !!(settings.statusLine && settings.statusLine.command && settings.statusLine.command.includes("statusline.js")), "reinstall to wire statusline");
1293
+ } catch (e) {
1294
+ check("Claude settings.json parseable", false, e.message);
1295
+ }
1296
+ }
1133
1297
 
1134
- // ── settings.json hook wiring ──────────────────────────
1135
- const settingsPath = path.join(CLAUDE_DIR, "settings.json");
1136
- if (fs.existsSync(settingsPath)) {
1137
- try {
1138
- const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
1139
- const wantEvents = ["SessionStart", "PreToolUse", "PreCompact", "Stop"];
1140
- for (const ev of wantEvents) {
1141
- const blocks = (settings.hooks || {})[ev] || [];
1142
- const hasQualia = blocks.some((b) =>
1143
- (b.hooks || []).some((h) => typeof h.command === "string" && h.command.includes(".claude")),
1144
- );
1145
- check(`settings.json hooks.${ev}`, hasQualia, "reinstall to wire hooks");
1298
+ if (label === "Codex" && fs.existsSync(path.join(home, "hooks.json"))) {
1299
+ try {
1300
+ const hooksJson = JSON.parse(fs.readFileSync(path.join(home, "hooks.json"), "utf8"));
1301
+ for (const ev of ["SessionStart", "PreToolUse", "Stop"]) {
1302
+ const blocks = (hooksJson.hooks || {})[ev] || [];
1303
+ const hasQualia = blocks.some((b) =>
1304
+ (b.hooks || []).some((h) => typeof h.command === "string" && h.command.includes(".codex")),
1305
+ );
1306
+ check(`Codex hooks.json ${ev}`, hasQualia, "reinstall to wire Codex hooks");
1307
+ }
1308
+ } catch (e) {
1309
+ check("Codex hooks.json parseable", false, e.message);
1146
1310
  }
1147
- } catch (e) {
1148
- check("settings.json parseable", false, e.message);
1149
1311
  }
1150
- } else {
1151
- check("settings.json", false, "Claude Code never ran here? Open Claude once first");
1152
1312
  }
1153
1313
 
1154
1314
  // ── Version vs. installed ──────────────────────────────
@@ -1263,7 +1423,7 @@ function cmdHelp() {
1263
1423
  console.log(` qualia-framework ${TEAL}install${RESET} Install or reinstall the framework`);
1264
1424
  console.log(` qualia-framework ${TEAL}update${RESET} Update to the latest version`);
1265
1425
  console.log(` qualia-framework ${TEAL}version${RESET} Show installed version + check for updates`);
1266
- console.log(` qualia-framework ${TEAL}uninstall${RESET} Clean removal from ~/.claude/ (${DIM}-y to skip prompts${RESET})`);
1426
+ console.log(` qualia-framework ${TEAL}uninstall${RESET} Clean removal from installed Claude/Codex homes (${DIM}-y to skip prompts${RESET})`);
1267
1427
  console.log(` qualia-framework ${TEAL}migrate${RESET} Wire current hook + env layout into ~/.claude/settings.json`);
1268
1428
  console.log(` qualia-framework ${TEAL}team${RESET} Manage team members (${DIM}list|add|remove${RESET})`);
1269
1429
  console.log(` qualia-framework ${TEAL}traces${RESET} View recent hook telemetry`);
@@ -1272,6 +1432,7 @@ function cmdHelp() {
1272
1432
  console.log(` qualia-framework ${TEAL}set-erp-key${RESET} Save/enable the ERP API key`);
1273
1433
  console.log(` qualia-framework ${TEAL}erp-ping${RESET} Verify ERP connectivity + API key`);
1274
1434
  console.log(` qualia-framework ${TEAL}erp-flush${RESET} Retry queued ERP report uploads (${DIM}show|clear${RESET})`);
1435
+ console.log(` qualia-framework ${TEAL}project-snapshot${RESET} Export/upload ERP admin project progress snapshot (${DIM}--write|--upload${RESET})`);
1275
1436
  console.log(` qualia-framework ${TEAL}doctor${RESET} Health-check the install (files, hooks, settings)`);
1276
1437
  console.log(` qualia-framework ${TEAL}flush${RESET} Promote daily-log → curated knowledge (memory layer)`);
1277
1438
  console.log("");
@@ -1341,6 +1502,10 @@ switch (cmd) {
1341
1502
  case "erp-retry":
1342
1503
  cmdErpFlush();
1343
1504
  break;
1505
+ case "project-snapshot":
1506
+ case "snapshot":
1507
+ cmdProjectSnapshot();
1508
+ break;
1344
1509
  case "doctor":
1345
1510
  case "health":
1346
1511
  case "health-check":