qualia-framework 6.1.0 → 6.2.9

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 (59) hide show
  1. package/README.md +39 -26
  2. package/agents/roadmapper.md +1 -1
  3. package/bin/cli.js +339 -200
  4. package/bin/codex-goal.js +92 -0
  5. package/bin/erp-retry.js +11 -3
  6. package/bin/install.js +483 -55
  7. package/bin/knowledge-flush.js +25 -13
  8. package/bin/knowledge.js +11 -1
  9. package/bin/project-snapshot.js +293 -0
  10. package/bin/qualia-ui.js +13 -2
  11. package/bin/report-payload.js +137 -0
  12. package/bin/state.js +8 -1
  13. package/bin/statusline.js +14 -2
  14. package/docs/changelog-v6.html +864 -0
  15. package/docs/ecosystem-operating-model.md +121 -0
  16. package/docs/erp-contract.md +74 -21
  17. package/docs/onboarding.html +1 -1
  18. package/docs/release.md +44 -0
  19. package/docs/reviews/v6.2.1-revival-audit.md +53 -0
  20. package/docs/reviews/v6.2.2-memory-erp-audit.md +41 -0
  21. package/docs/reviews/v6.2.3-erp-id-guard.md +15 -0
  22. package/guide.md +16 -4
  23. package/hooks/auto-update.js +14 -7
  24. package/hooks/branch-guard.js +10 -2
  25. package/hooks/env-empty-guard.js +10 -1
  26. package/hooks/git-guardrails.js +10 -1
  27. package/hooks/migration-guard.js +4 -1
  28. package/hooks/pre-deploy-gate.js +38 -1
  29. package/hooks/pre-push.js +56 -157
  30. package/hooks/session-start.js +22 -14
  31. package/hooks/stop-session-log.js +11 -3
  32. package/hooks/supabase-destructive-guard.js +11 -1
  33. package/hooks/vercel-account-guard.js +12 -3
  34. package/package.json +3 -2
  35. package/rules/codex-goal.md +46 -0
  36. package/skills/qualia-build/SKILL.md +4 -0
  37. package/skills/qualia-feature/SKILL.md +4 -0
  38. package/skills/qualia-map/SKILL.md +1 -1
  39. package/skills/qualia-milestone/SKILL.md +1 -1
  40. package/skills/qualia-optimize/SKILL.md +1 -1
  41. package/skills/qualia-plan/SKILL.md +4 -0
  42. package/skills/qualia-polish/SKILL.md +2 -2
  43. package/skills/qualia-report/SKILL.md +6 -43
  44. package/skills/qualia-road/SKILL.md +1 -1
  45. package/skills/qualia-verify/SKILL.md +1 -1
  46. package/templates/help.html +1 -1
  47. package/templates/knowledge/agents.md +3 -3
  48. package/templates/knowledge/index.md +1 -1
  49. package/templates/tracking.json +3 -0
  50. package/templates/work-packet.md +46 -0
  51. package/tests/bin.test.sh +411 -13
  52. package/tests/hooks.test.sh +1 -8
  53. package/tests/install-smoke.test.sh +137 -0
  54. package/tests/published-install-smoke.test.sh +126 -0
  55. package/tests/refs.test.sh +42 -0
  56. package/tests/run-all.sh +1 -0
  57. package/tests/runner.js +19 -33
  58. package/tests/state.test.sh +4 -1
  59. package/hooks/pre-compact.js +0 -127
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;
23
42
  try {
24
- return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
43
+ return JSON.parse(fs.readFileSync(configFile, "utf8"));
25
44
  } catch {
26
45
  return {};
27
46
  }
28
47
  }
29
48
 
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 {}
49
+ function readConfigAt(home) {
50
+ try {
51
+ return JSON.parse(fs.readFileSync(configFileForHome(home), "utf8"));
52
+ } catch {
53
+ return {};
54
+ }
55
+ }
56
+
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,7 +202,6 @@ 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",
145
207
  "env-empty-guard.js",
@@ -148,6 +210,7 @@ const QUALIA_HOOK_FILES = [
148
210
  ];
149
211
  const QUALIA_LEGACY_HOOK_FILES = [
150
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
151
214
  ];
152
215
 
153
216
  // Qualia agents — only these are removed.
@@ -162,6 +225,7 @@ const QUALIA_AGENT_FILES = [
162
225
  "roadmapper.md",
163
226
  "visual-evaluator.md",
164
227
  ];
228
+ const QUALIA_CODEX_AGENT_FILES = QUALIA_AGENT_FILES.map((f) => f.replace(/\.md$/, ".toml"));
165
229
 
166
230
  // Qualia bin scripts.
167
231
  const QUALIA_BIN_FILES = [
@@ -174,12 +238,15 @@ const QUALIA_BIN_FILES = [
174
238
  "agent-runs.js",
175
239
  "slop-detect.mjs",
176
240
  "erp-retry.js",
241
+ "report-payload.js",
242
+ "project-snapshot.js",
177
243
  ];
178
244
 
179
245
  // Qualia rules — security, deployment, infra, grounding, plus the v4.5.0 design substrate.
180
246
  // frontend.md and design-reference.md are kept for backward compat; new projects use design-laws/brand/product/rubric.
181
247
  const QUALIA_RULE_FILES = [
182
248
  "security.md", "deployment.md", "infrastructure.md", "grounding.md",
249
+ "speed.md", "architecture.md", "trust-boundary.md", "one-opinion.md",
183
250
  "frontend.md", "design-reference.md",
184
251
  "design-laws.md", "design-brand.md", "design-product.md", "design-rubric.md",
185
252
  ];
@@ -219,46 +286,55 @@ function safeRmDir(p, counters) {
219
286
  }
220
287
  }
221
288
 
222
- function cleanSettingsJson(counters) {
223
- const settingsPath = path.join(CLAUDE_DIR, "settings.json");
224
- if (!fs.existsSync(settingsPath)) return;
225
- let settings;
226
- try {
227
- settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
228
- } catch (e) {
229
- counters.errors.push(`settings.json: ${e.message}`);
230
- return;
231
- }
232
-
233
- // Only remove entries that point at qualia paths. Leave everything else.
234
- const isQualiaCommand = (cmd) =>
235
- 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
+ }
236
297
 
237
- const filterHookArray = (arr) => {
238
- if (!Array.isArray(arr)) return arr;
239
- 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
240
305
  .map((entry) => {
241
306
  if (!entry || !Array.isArray(entry.hooks)) return entry;
242
307
  const hooks = entry.hooks.filter((h) => !isQualiaCommand(h && h.command));
308
+ if (hooks.length !== entry.hooks.length) changed = true;
243
309
  return { ...entry, hooks };
244
310
  })
245
311
  .filter((entry) => Array.isArray(entry.hooks) && entry.hooks.length > 0);
246
- };
247
-
248
- if (settings.hooks && typeof settings.hooks === "object") {
249
- // Iterate every hook event key, not a hardcoded subset — future hook
250
- // events added by Claude Code or the framework get cleaned automatically.
251
- for (const key of Object.keys(settings.hooks)) {
252
- const cleaned = filterHookArray(settings.hooks[key]);
253
- if (cleaned && cleaned.length > 0) {
254
- settings.hooks[key] = cleaned;
255
- } else {
256
- delete settings.hooks[key];
257
- }
312
+ if (cleaned.length > 0) {
313
+ root[hookKey][key] = cleaned;
314
+ } else {
315
+ delete root[hookKey][key];
316
+ changed = true;
258
317
  }
259
- // If hooks is now empty, remove it entirely.
260
- if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
261
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");
262
338
 
263
339
  // Status line — only drop it if it points at our renderer.
264
340
  if (settings.statusLine && typeof settings.statusLine === "object") {
@@ -282,19 +358,40 @@ function cleanSettingsJson(counters) {
282
358
  }
283
359
  }
284
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
+
285
380
  async function cmdUninstall() {
286
381
  banner();
287
382
 
288
383
  const args = process.argv.slice(3);
289
384
  const skipConfirm = args.includes("-y") || args.includes("--yes");
290
385
 
386
+ const homes = frameworkHomes();
291
387
  const cfg = readConfig();
292
388
  console.log("");
293
389
  if (cfg.installed_by) {
294
390
  console.log(` ${DIM}User:${RESET} ${WHITE}${cfg.installed_by}${RESET} ${DIM}(${cfg.role || "?"})${RESET}`);
295
391
  } else {
296
- 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}`);
297
393
  }
394
+ console.log(` ${DIM}Targets:${RESET} ${WHITE}${homes.map(installLabel).join(" · ")}${RESET}`);
298
395
  console.log("");
299
396
 
300
397
  if (!skipConfirm) {
@@ -320,62 +417,60 @@ async function cmdUninstall() {
320
417
  console.log(` ${DIM}Removing framework files...${RESET}`);
321
418
  console.log("");
322
419
 
323
- const counters = { filesRemoved: 0, dirsRemoved: 0, settingsCleaned: false, errors: [] };
420
+ const counters = { filesRemoved: 0, dirsRemoved: 0, settingsCleaned: false, hooksJsonCleaned: false, errors: [] };
324
421
 
325
- // Skills any directory starting with "qualia" under ~/.claude/skills/.
326
- const skillsDir = path.join(CLAUDE_DIR, "skills");
327
- try {
328
- if (fs.existsSync(skillsDir)) {
329
- for (const name of fs.readdirSync(skillsDir)) {
330
- if (name === "qualia" || name.startsWith("qualia-")) {
331
- 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
+ }
332
430
  }
333
431
  }
432
+ } catch (e) {
433
+ counters.errors.push(`${installLabel(home)} skills scan: ${e.message}`);
334
434
  }
335
- } catch (e) {
336
- counters.errors.push(`skills scan: ${e.message}`);
337
- }
338
435
 
339
- // Agents only the Qualia ones.
340
- for (const f of QUALIA_AGENT_FILES) {
341
- safeUnlink(path.join(CLAUDE_DIR, "agents", f), counters);
342
- }
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
+ }
343
442
 
344
- // Hooks current set plus any legacy hook filenames from older versions.
345
- for (const f of [...QUALIA_HOOK_FILES, ...QUALIA_LEGACY_HOOK_FILES]) {
346
- safeUnlink(path.join(CLAUDE_DIR, "hooks", f), counters);
347
- }
443
+ for (const f of [...QUALIA_HOOK_FILES, ...QUALIA_LEGACY_HOOK_FILES]) {
444
+ safeUnlink(path.join(home, "hooks", f), counters);
445
+ }
348
446
 
349
- // Bin scripts only the Qualia ones.
350
- for (const f of QUALIA_BIN_FILES) {
351
- safeUnlink(path.join(CLAUDE_DIR, "bin", f), counters);
352
- }
447
+ for (const f of QUALIA_BIN_FILES) {
448
+ safeUnlink(path.join(home, "bin", f), counters);
449
+ }
353
450
 
354
- // Rules all 4.
355
- for (const f of QUALIA_RULE_FILES) {
356
- safeUnlink(path.join(CLAUDE_DIR, "rules", f), counters);
357
- }
451
+ for (const f of QUALIA_RULE_FILES) {
452
+ safeUnlink(path.join(home, "rules", f), counters);
453
+ }
358
454
 
359
- // Templates directory (entire).
360
- 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);
361
458
 
362
- // Knowledge directory (optional preservation).
363
- if (!preserveKnowledge) {
364
- safeRmDir(path.join(CLAUDE_DIR, "knowledge"), counters);
365
- }
459
+ if (!preserveKnowledge) {
460
+ safeRmDir(path.join(home, "knowledge"), counters);
461
+ }
366
462
 
367
- // Config + state files.
368
- safeUnlink(path.join(CLAUDE_DIR, ".qualia-config.json"), counters);
369
- safeUnlink(path.join(CLAUDE_DIR, ".qualia-last-update-check"), counters);
370
- safeUnlink(path.join(CLAUDE_DIR, ".erp-api-key"), counters);
371
- safeUnlink(path.join(CLAUDE_DIR, ".qualia-team.json"), counters);
372
- 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);
373
468
 
374
- // Traces directory.
375
- safeRmDir(path.join(CLAUDE_DIR, ".qualia-traces"), counters);
469
+ safeRmDir(path.join(home, ".qualia-traces"), counters);
376
470
 
377
- // Clean settings.json surgically.
378
- cleanSettingsJson(counters);
471
+ if (isCodexHome(home)) cleanCodexHooksJson(home, counters);
472
+ else cleanSettingsJson(home, counters);
473
+ }
379
474
 
380
475
  // Summary.
381
476
  console.log("");
@@ -386,6 +481,9 @@ async function cmdUninstall() {
386
481
  console.log(
387
482
  ` ${DIM}settings.json:${RESET} ${counters.settingsCleaned ? `${GREEN}cleaned ✓${RESET}` : `${DIM}not present${RESET}`}`
388
483
  );
484
+ console.log(
485
+ ` ${DIM}hooks.json:${RESET} ${counters.hooksJsonCleaned ? `${GREEN}cleaned ✓${RESET}` : `${DIM}not present${RESET}`}`
486
+ );
389
487
  if (preserveKnowledge) {
390
488
  console.log(` ${DIM}Knowledge base:${RESET} ${GREEN}preserved ✓${RESET}`);
391
489
  } else {
@@ -401,14 +499,13 @@ async function cmdUninstall() {
401
499
  }
402
500
 
403
501
  console.log("");
404
- console.log(
405
- ` ${YELLOW}Manual step:${RESET} edit ${WHITE}~/.claude/CLAUDE.md${RESET} to remove the Qualia Framework section if desired.`
406
- );
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.`);
407
503
  console.log("");
408
504
  }
409
505
 
410
506
  // ─── Team Management ────────────────────────────────────
411
- // 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.
412
509
  // Falls back to embedded defaults in install.js.
413
510
 
414
511
  function getDefaultTeam() {
@@ -422,19 +519,23 @@ function getDefaultTeam() {
422
519
  }
423
520
 
424
521
  function readTeamFile() {
425
- const teamFile = path.join(CLAUDE_DIR, ".qualia-team.json");
426
- try {
427
- if (fs.existsSync(teamFile)) {
428
- const data = JSON.parse(fs.readFileSync(teamFile, "utf8"));
429
- if (data && typeof data === "object" && Object.keys(data).length > 0) return data;
430
- }
431
- } 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
+ }
432
531
  return null;
433
532
  }
434
533
 
435
534
  function writeTeamFile(team) {
436
- if (!fs.existsSync(CLAUDE_DIR)) fs.mkdirSync(CLAUDE_DIR, { recursive: true });
437
- 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
+ }
438
539
  }
439
540
 
440
541
  function parseTeamArgs(argv) {
@@ -456,7 +557,7 @@ function cmdTeam() {
456
557
  console.log("");
457
558
  const team = readTeamFile();
458
559
  const source = team || getDefaultTeam();
459
- const label = team ? "team file" : "embedded defaults";
560
+ const label = team ? "installed team file" : "embedded defaults";
460
561
  console.log(` ${DIM}Source: ${label}${RESET}`);
461
562
  console.log("");
462
563
  for (const [code, member] of Object.entries(source)) {
@@ -516,7 +617,7 @@ function cmdTeam() {
516
617
  function cmdTraces() {
517
618
  banner();
518
619
  console.log("");
519
- const tracesDir = path.join(CLAUDE_DIR, ".qualia-traces");
620
+ const tracesDir = path.join(primaryInstallHome(), ".qualia-traces");
520
621
  if (!fs.existsSync(tracesDir)) {
521
622
  console.log(` ${DIM}No traces found. Traces are written by hooks during normal operation.${RESET}`);
522
623
  console.log("");
@@ -552,6 +653,15 @@ function cmdMigrate() {
552
653
 
553
654
  const settingsPath = path.join(CLAUDE_DIR, "settings.json");
554
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
+ }
555
665
  console.log(` ${RED}✗${RESET} No settings.json found. Run ${TEAL}qualia-framework install${RESET} first.`);
556
666
  console.log("");
557
667
  process.exit(1);
@@ -667,23 +777,22 @@ function cmdMigrate() {
667
777
  }
668
778
  }
669
779
 
670
- // Check PreCompact hook
671
- if (!settings.hooks.PreCompact || !Array.isArray(settings.hooks.PreCompact)) {
672
- settings.hooks.PreCompact = [{ matcher: "compact", hooks: [{ type: "command", command: nodeCmd("pre-compact.js"), timeout: 15 }] }];
673
- changes++;
674
- console.log(` ${GREEN}+${RESET} Added PreCompact hook`);
675
- } else {
676
- let compactEntry = settings.hooks.PreCompact.find(e => e.matcher === "compact");
677
- if (!compactEntry) {
678
- compactEntry = { matcher: "compact", hooks: [] };
679
- settings.hooks.PreCompact.push(compactEntry);
680
- }
681
- if (!compactEntry.hooks) compactEntry.hooks = [];
682
- const exists = compactEntry.hooks.some(h => extractScriptName(h.command) === "pre-compact.js");
683
- if (!exists) {
684
- 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) {
685
794
  changes++;
686
- console.log(` ${GREEN}+${RESET} Wired pre-compact.js into PreCompact`);
795
+ console.log(` ${GREEN}-${RESET} Removed legacy pre-compact.js from PreCompact`);
687
796
  }
688
797
  }
689
798
 
@@ -764,7 +873,7 @@ function cmdAnalytics() {
764
873
  banner();
765
874
  console.log("");
766
875
 
767
- const tracesDir = path.join(CLAUDE_DIR, ".qualia-traces");
876
+ const tracesDir = path.join(primaryInstallHome(), ".qualia-traces");
768
877
  if (!fs.existsSync(tracesDir)) {
769
878
  console.log(` ${DIM}No traces found. Analytics require hook telemetry data.${RESET}`);
770
879
  console.log(` ${DIM}Traces are collected automatically during normal framework use.${RESET}`);
@@ -854,11 +963,12 @@ async function cmdErpPing() {
854
963
  banner();
855
964
  console.log("");
856
965
 
966
+ const installHome = primaryInstallHome();
857
967
  const cfg = readConfig();
858
968
  const args = new Set(process.argv.slice(3));
859
969
  const erpUrl = (cfg.erp && cfg.erp.url) || "https://portal.qualiasolutions.net";
860
970
  let erpEnabled = !(cfg.erp && cfg.erp.enabled === false);
861
- const keyFile = path.join(CLAUDE_DIR, ".erp-api-key");
971
+ const keyFile = path.join(installHome, ".erp-api-key");
862
972
 
863
973
  console.log(` ${DIM}URL:${RESET} ${WHITE}${erpUrl}${RESET}`);
864
974
  console.log(` ${DIM}Enabled:${RESET} ${erpEnabled ? `${GREEN}yes${RESET}` : `${YELLOW}no (erp.enabled=false)${RESET}`}`);
@@ -989,17 +1099,21 @@ function cmdSetErpKey() {
989
1099
  banner();
990
1100
  console.log("");
991
1101
 
992
- const keyFile = path.join(CLAUDE_DIR, ".erp-api-key");
1102
+ const homes = installedHomes();
993
1103
  const rawArgs = process.argv.slice(3);
994
1104
  const clear = rawArgs.includes("--clear");
995
1105
 
996
- 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
+ }
997
1109
 
998
1110
  if (clear) {
999
- try { fs.unlinkSync(keyFile); } catch {}
1000
- const cfg = readConfig();
1001
- cfg.erp = { ...(cfg.erp || {}), enabled: false, url: (cfg.erp && cfg.erp.url) || "https://portal.qualiasolutions.net", api_key_file: ".erp-api-key" };
1002
- 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
+ }
1003
1117
  console.log(` ${GREEN}✓${RESET} ERP key removed and ERP disabled.`);
1004
1118
  console.log("");
1005
1119
  return;
@@ -1036,19 +1150,21 @@ function cmdSetErpKey() {
1036
1150
  console.log(` ${YELLOW}!${RESET} Key looks short (${key.length} bytes). Saving anyway.`);
1037
1151
  }
1038
1152
 
1039
- fs.writeFileSync(keyFile, key, { mode: 0o600 });
1040
- try { fs.chmodSync(keyFile, 0o600); } catch {}
1041
-
1042
- const cfg = readConfig();
1043
- cfg.erp = {
1044
- ...(cfg.erp || {}),
1045
- enabled: true,
1046
- url: (cfg.erp && cfg.erp.url) || "https://portal.qualiasolutions.net",
1047
- api_key_file: ".erp-api-key",
1048
- };
1049
- 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
+ }
1050
1166
 
1051
- 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}`);
1052
1168
  console.log(` ${DIM}Verify with:${RESET} ${TEAL}qualia-framework erp-ping${RESET}`);
1053
1169
  console.log("");
1054
1170
  }
@@ -1066,7 +1182,7 @@ function cmdSetErpKey() {
1066
1182
  // retry stranded reports on demand (e.g., after the ERP came back online,
1067
1183
  // or after rotating the API key). All args pass through.
1068
1184
  function cmdErpFlush() {
1069
- const retryScript = path.join(CLAUDE_DIR, "bin", "erp-retry.js");
1185
+ const retryScript = path.join(primaryInstallHome(), "bin", "erp-retry.js");
1070
1186
  if (!fs.existsSync(retryScript)) {
1071
1187
  console.log(` ${RED}✗${RESET} erp-retry.js not installed at ${retryScript}`);
1072
1188
  console.log(` ${DIM}Run: npx qualia-framework@latest install${RESET}`);
@@ -1084,8 +1200,25 @@ function cmdErpFlush() {
1084
1200
  process.exit(r.status || 0);
1085
1201
  }
1086
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
+
1087
1220
  function cmdFlush() {
1088
- const flushScript = path.join(CLAUDE_DIR, "bin", "knowledge-flush.js");
1221
+ const flushScript = path.join(primaryInstallHome(), "bin", "knowledge-flush.js");
1089
1222
  if (!fs.existsSync(flushScript)) {
1090
1223
  console.log(` ${RED}✗${RESET} knowledge-flush.js not installed at ${flushScript}`);
1091
1224
  console.log(` ${DIM}Run: npx qualia-framework@latest install${RESET}`);
@@ -1111,70 +1244,71 @@ function cmdDoctor() {
1111
1244
  if (!ok) issues.push({ label, hint });
1112
1245
  }
1113
1246
 
1114
- // ── Critical files (the same set session-start.js validates) ──
1115
- const criticalFiles = [
1116
- path.join(CLAUDE_DIR, "rules", "grounding.md"),
1117
- path.join(CLAUDE_DIR, "rules", "security.md"),
1118
- path.join(CLAUDE_DIR, "rules", "frontend.md"),
1119
- path.join(CLAUDE_DIR, "rules", "deployment.md"),
1120
- path.join(CLAUDE_DIR, "bin", "state.js"),
1121
- path.join(CLAUDE_DIR, "bin", "qualia-ui.js"),
1122
- path.join(CLAUDE_DIR, "bin", "statusline.js"),
1123
- path.join(CLAUDE_DIR, "bin", "knowledge.js"),
1124
- path.join(CLAUDE_DIR, "bin", "knowledge-flush.js"),
1125
- path.join(CLAUDE_DIR, "bin", "erp-retry.js"),
1126
- path.join(CLAUDE_DIR, "CLAUDE.md"),
1127
- CONFIG_FILE,
1128
- ];
1129
- for (const f of criticalFiles) {
1130
- check(
1131
- `${path.relative(CLAUDE_DIR, f) || f}`,
1132
- fs.existsSync(f),
1133
- "run: npx qualia-framework@latest install",
1134
- );
1135
- }
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
+ }
1136
1277
 
1137
- // ── Hooks ─────────────────────────────────────────────
1138
- for (const h of QUALIA_HOOK_FILES) {
1139
- check(
1140
- `hooks/${h}`,
1141
- fs.existsSync(path.join(CLAUDE_DIR, "hooks", h)),
1142
- "reinstall: npx qualia-framework@latest install",
1143
- );
1144
- }
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
+ }
1145
1281
 
1146
- // ── Knowledge layer ────────────────────────────────────
1147
- const knowledgeFiles = [
1148
- path.join(CLAUDE_DIR, "knowledge", "agents.md"),
1149
- path.join(CLAUDE_DIR, "knowledge", "index.md"),
1150
- path.join(CLAUDE_DIR, "knowledge", "daily-log"),
1151
- ];
1152
- for (const f of knowledgeFiles) {
1153
- check(
1154
- `knowledge/${path.basename(f)}${fs.existsSync(f) && fs.statSync(f).isDirectory() ? "/" : ""}`,
1155
- fs.existsSync(f),
1156
- "reinstall to initialize the memory layer: npx qualia-framework@latest install",
1157
- );
1158
- }
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
+ }
1159
1297
 
1160
- // ── settings.json hook wiring ──────────────────────────
1161
- const settingsPath = path.join(CLAUDE_DIR, "settings.json");
1162
- if (fs.existsSync(settingsPath)) {
1163
- try {
1164
- const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
1165
- const wantEvents = ["SessionStart", "PreToolUse", "PreCompact", "Stop"];
1166
- for (const ev of wantEvents) {
1167
- const blocks = (settings.hooks || {})[ev] || [];
1168
- const hasQualia = blocks.some((b) =>
1169
- (b.hooks || []).some((h) => typeof h.command === "string" && h.command.includes(".claude")),
1170
- );
1171
- 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);
1172
1310
  }
1173
- } catch (e) {
1174
- check("settings.json parseable", false, e.message);
1175
1311
  }
1176
- } else {
1177
- check("settings.json", false, "Claude Code never ran here? Open Claude once first");
1178
1312
  }
1179
1313
 
1180
1314
  // ── Version vs. installed ──────────────────────────────
@@ -1289,7 +1423,7 @@ function cmdHelp() {
1289
1423
  console.log(` qualia-framework ${TEAL}install${RESET} Install or reinstall the framework`);
1290
1424
  console.log(` qualia-framework ${TEAL}update${RESET} Update to the latest version`);
1291
1425
  console.log(` qualia-framework ${TEAL}version${RESET} Show installed version + check for updates`);
1292
- 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})`);
1293
1427
  console.log(` qualia-framework ${TEAL}migrate${RESET} Wire current hook + env layout into ~/.claude/settings.json`);
1294
1428
  console.log(` qualia-framework ${TEAL}team${RESET} Manage team members (${DIM}list|add|remove${RESET})`);
1295
1429
  console.log(` qualia-framework ${TEAL}traces${RESET} View recent hook telemetry`);
@@ -1298,6 +1432,7 @@ function cmdHelp() {
1298
1432
  console.log(` qualia-framework ${TEAL}set-erp-key${RESET} Save/enable the ERP API key`);
1299
1433
  console.log(` qualia-framework ${TEAL}erp-ping${RESET} Verify ERP connectivity + API key`);
1300
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})`);
1301
1436
  console.log(` qualia-framework ${TEAL}doctor${RESET} Health-check the install (files, hooks, settings)`);
1302
1437
  console.log(` qualia-framework ${TEAL}flush${RESET} Promote daily-log → curated knowledge (memory layer)`);
1303
1438
  console.log("");
@@ -1367,6 +1502,10 @@ switch (cmd) {
1367
1502
  case "erp-retry":
1368
1503
  cmdErpFlush();
1369
1504
  break;
1505
+ case "project-snapshot":
1506
+ case "snapshot":
1507
+ cmdProjectSnapshot();
1508
+ break;
1370
1509
  case "doctor":
1371
1510
  case "health":
1372
1511
  case "health-check":