qualia-framework 4.0.0 → 4.0.5

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 (47) hide show
  1. package/CLAUDE.md +23 -11
  2. package/agents/plan-checker.md +1 -1
  3. package/agents/roadmapper.md +10 -5
  4. package/bin/cli.js +139 -17
  5. package/bin/install.js +47 -47
  6. package/bin/qualia-ui.js +2 -2
  7. package/bin/state.js +126 -9
  8. package/bin/statusline.js +63 -38
  9. package/docs/erp-contract.md +49 -2
  10. package/guide.md +1 -1
  11. package/hooks/migration-guard.js +23 -9
  12. package/hooks/pre-compact.js +39 -11
  13. package/hooks/pre-deploy-gate.js +3 -4
  14. package/hooks/pre-push.js +6 -3
  15. package/hooks/session-start.js +8 -8
  16. package/package.json +1 -1
  17. package/rules/frontend.md +5 -13
  18. package/skills/qualia/SKILL.md +5 -0
  19. package/skills/qualia-build/SKILL.md +10 -0
  20. package/skills/qualia-debug/SKILL.md +6 -0
  21. package/skills/qualia-design/SKILL.md +9 -1
  22. package/skills/qualia-discuss/SKILL.md +6 -0
  23. package/skills/qualia-handoff/SKILL.md +5 -0
  24. package/skills/qualia-help/SKILL.md +18 -4
  25. package/skills/qualia-idk/SKILL.md +6 -0
  26. package/skills/qualia-learn/SKILL.md +6 -0
  27. package/skills/qualia-map/SKILL.md +7 -0
  28. package/skills/qualia-milestone/SKILL.md +6 -0
  29. package/skills/qualia-new/SKILL.md +31 -4
  30. package/skills/qualia-optimize/SKILL.md +8 -0
  31. package/skills/qualia-pause/SKILL.md +5 -0
  32. package/skills/qualia-plan/SKILL.md +11 -1
  33. package/skills/qualia-polish/SKILL.md +8 -0
  34. package/skills/qualia-quick/SKILL.md +7 -0
  35. package/skills/qualia-report/SKILL.md +146 -60
  36. package/skills/qualia-research/SKILL.md +7 -0
  37. package/skills/qualia-resume/SKILL.md +3 -0
  38. package/skills/qualia-review/SKILL.md +7 -0
  39. package/skills/qualia-ship/SKILL.md +5 -0
  40. package/skills/qualia-skill-new/SKILL.md +6 -0
  41. package/skills/qualia-task/SKILL.md +8 -1
  42. package/skills/qualia-test/SKILL.md +7 -0
  43. package/skills/qualia-verify/SKILL.md +8 -0
  44. package/templates/help.html +4 -4
  45. package/templates/tracking.json +1 -0
  46. package/tests/hooks.test.sh +5 -5
  47. package/tests/runner.js +310 -3
package/CLAUDE.md CHANGED
@@ -23,25 +23,37 @@ Next.js 16+, React 19, TypeScript, Supabase, Vercel. Voice: Retell AI, ElevenLab
23
23
 
24
24
  ## The Road (how projects flow)
25
25
 
26
+ v4 hierarchy: **Project → Journey → Milestones (2–5, Handoff always last) → Phases (2–5 tasks each) → Tasks (one commit, one verification contract).**
27
+
26
28
  ```
27
- /qualia-new set up project
29
+ /qualia-new kickoff + parallel research + JOURNEY.md (all milestones upfront)
30
+ add --auto to chain the whole road end-to-end
28
31
 
29
- For each phase:
30
- /qualia-plan → plan the phase (planner agent, fresh context)
31
- /qualia-build → build it (builder subagents per task, fresh context each)
32
- /qualia-verify verify it works (verifier agent, goal-backward checks)
32
+ For each milestone, for each phase:
33
+ /qualia-plan → plan the phase (planner + plan-checker revision loop, fresh context)
34
+ /qualia-build → build it (builder subagents per task, wave-based parallel)
35
+ /qualia-verify goal-backward check (verifier agent, fresh context)
33
36
 
34
- /qualia-polish design/UX pass
35
- /qualia-ship → deploy to production
36
- /qualia-handoff deliver to client
37
+ /qualia-milestone close milestone, archive artifacts, prep next (human gate)
38
+ (repeat for each milestone until Handoff)
39
+ Final milestone = Handoff:
40
+ /qualia-polish → design/UX pass (Phase 1 of Handoff)
41
+ (content + SEO) → Phase 2
42
+ (final QA) → Phase 3
43
+ /qualia-ship → deploy to production (quality gates → deploy → verify)
44
+ /qualia-handoff → 4 deliverables: credentials, doc, final update, report
37
45
 
38
46
  Done.
39
47
 
40
- Lost? → /qualia (tells you exactly what's next)
41
- Quick fix? → /qualia-quick (skip planning for small tasks)
42
- End of day? → /qualia-report (mandatory before clock-out)
48
+ Lost? → /qualia (state router — tells you the next command)
49
+ Stuck/weird? → /qualia-idk (diagnostic spawns plan-view + code-view agents in parallel)
50
+ Quick fix? → /qualia-quick (skip planning for small tasks)
51
+ Paused? → /qualia-resume (restore from .continue-here.md or STATE.md)
52
+ End of day? → /qualia-report (mandatory before clock-out; writes ERP payload)
43
53
  ```
44
54
 
55
+ **Human gates:** journey approval after `/qualia-new`, then one at each milestone boundary via `/qualia-milestone`. `--auto` runs everything between gates automatically.
56
+
45
57
  ## Context Isolation
46
58
  Every task runs in a fresh subagent context. Task 50 gets the same quality as Task 1.
47
59
  - Planner gets: PROJECT.md + phase requirements
@@ -34,7 +34,7 @@ Plan must have YAML frontmatter with:
34
34
 
35
35
  **FAIL if:** frontmatter missing, incomplete, or `goal` differs from ROADMAP.md.
36
36
 
37
- ### Rule 2: Every task has the 6 mandatory story-file fields
37
+ ### Rule 2: Every task has the 7 mandatory story-file fields
38
38
 
39
39
  Each `## Task N — title` block must include ALL of these:
40
40
 
@@ -85,14 +85,18 @@ Use the research SUMMARY.md as your starting point. Don't force-fit the template
85
85
  - **Phases** — 2-5 phases. For Milestone 1, include full detail (goal + success criteria). For M2..M{N-1}, names + one-line goals are enough (progressive detail — full detail gets written when that milestone opens). For Handoff, use the fixed 4-phase template.
86
86
  - **Requirements covered** — list the REQ-IDs this milestone delivers
87
87
 
88
- ### 4. Build ROADMAP.md — ONLY Milestone 1's phases (fully detailed)
88
+ ### 4. Build ROADMAP.md — Milestone 1's phases (progressive detail by default)
89
89
 
90
- The current milestone gets full phase detail. Future milestones stay as sketches in JOURNEY.md until they open.
90
+ Check the `<full_detail>` flag in your prompt:
91
91
 
92
- For each phase in Milestone 1:
92
+ **`full_detail=false` (default):** Only Milestone 1 gets full phase detail in ROADMAP.md. Future milestones stay as sketches in JOURNEY.md until they open. This matches progressive-detail planning and is the recommended default.
93
+
94
+ **`full_detail=true`:** Write full phase detail for EVERY milestone (M1..Handoff) in ROADMAP.md, sectioned by milestone. Use when the client wants a fully-committed plan at kickoff. Trade-off: M2+ detail often needs revision as M1 ships — flag this in your summary.
95
+
96
+ For each phase in the milestone(s) you're detailing:
93
97
  - **Name** + **goal** (one line)
94
98
  - **Success criteria** — 2-5 observable user-facing behaviors
95
- - **Requirements covered** — REQ-IDs from REQUIREMENTS.md Milestone 1 section
99
+ - **Requirements covered** — REQ-IDs from REQUIREMENTS.md section for that milestone
96
100
 
97
101
  ### 5. Validate Coverage
98
102
 
@@ -104,7 +108,8 @@ Before writing, verify:
104
108
  - [ ] Final milestone is literally named "Handoff" with the 4 standard phases
105
109
  - [ ] No milestone depends on a later milestone
106
110
  - [ ] Milestone 1 has full phase-level detail (goals + success criteria) ready for `/qualia-plan 1`
107
- - [ ] M2..M{N-1} have phase names + one-line goals (sketch, not full detail)
111
+ - [ ] If `full_detail=false` (default): M2..M{N-1} have phase names + one-line goals (sketch, not full detail)
112
+ - [ ] If `full_detail=true`: every milestone in ROADMAP.md has full phase detail; flag this mode explicitly in your summary output
108
113
 
109
114
  If any check fails, fix it. The orchestrator trusts your output.
110
115
 
package/bin/cli.js CHANGED
@@ -126,21 +126,35 @@ function cmdUpdate() {
126
126
  // non-Qualia entries in settings.json (other hooks, user env vars, etc.).
127
127
  // --yes / -y skips the confirmation prompt for scripted use.
128
128
 
129
- // 8 Qualia hook filenames — only these are removed from ~/.claude/hooks/,
130
- // any other hooks the user dropped in there are left alone.
129
+ // Current Qualia hook filenames — only these are removed from ~/.claude/hooks/,
130
+ // any other hooks the user dropped in there are left alone. The LEGACY set
131
+ // lists hooks that were shipped by older framework versions but have since
132
+ // been removed; uninstall still tries to clean them so old installs get a
133
+ // clean removal.
131
134
  const QUALIA_HOOK_FILES = [
132
135
  "session-start.js",
133
136
  "auto-update.js",
134
137
  "branch-guard.js",
135
138
  "pre-push.js",
136
- "block-env-edit.js",
137
139
  "migration-guard.js",
138
140
  "pre-deploy-gate.js",
139
141
  "pre-compact.js",
140
142
  ];
143
+ const QUALIA_LEGACY_HOOK_FILES = [
144
+ "block-env-edit.js", // removed in v3.2.0
145
+ ];
141
146
 
142
- // 4 Qualia agents — only these are removed.
143
- const QUALIA_AGENT_FILES = ["planner.md", "builder.md", "verifier.md", "qa-browser.md"];
147
+ // 8 Qualia agents — only these are removed.
148
+ const QUALIA_AGENT_FILES = [
149
+ "planner.md",
150
+ "builder.md",
151
+ "verifier.md",
152
+ "qa-browser.md",
153
+ "plan-checker.md",
154
+ "researcher.md",
155
+ "research-synthesizer.md",
156
+ "roadmapper.md",
157
+ ];
144
158
 
145
159
  // 3 Qualia bin scripts.
146
160
  const QUALIA_BIN_FILES = ["state.js", "qualia-ui.js", "statusline.js"];
@@ -210,14 +224,14 @@ function cleanSettingsJson(counters) {
210
224
  };
211
225
 
212
226
  if (settings.hooks && typeof settings.hooks === "object") {
213
- for (const key of ["SessionStart", "PreToolUse", "PreCompact"]) {
214
- if (settings.hooks[key]) {
215
- const cleaned = filterHookArray(settings.hooks[key]);
216
- if (cleaned && cleaned.length > 0) {
217
- settings.hooks[key] = cleaned;
218
- } else {
219
- delete settings.hooks[key];
220
- }
227
+ // Iterate every hook event key, not a hardcoded subset — future hook
228
+ // events added by Claude Code or the framework get cleaned automatically.
229
+ for (const key of Object.keys(settings.hooks)) {
230
+ const cleaned = filterHookArray(settings.hooks[key]);
231
+ if (cleaned && cleaned.length > 0) {
232
+ settings.hooks[key] = cleaned;
233
+ } else {
234
+ delete settings.hooks[key];
221
235
  }
222
236
  }
223
237
  // If hooks is now empty, remove it entirely.
@@ -305,8 +319,8 @@ async function cmdUninstall() {
305
319
  safeUnlink(path.join(CLAUDE_DIR, "agents", f), counters);
306
320
  }
307
321
 
308
- // Hooks — only the 8 Qualia ones.
309
- for (const f of QUALIA_HOOK_FILES) {
322
+ // Hooks — current set plus any legacy hook filenames from older versions.
323
+ for (const f of [...QUALIA_HOOK_FILES, ...QUALIA_LEGACY_HOOK_FILES]) {
310
324
  safeUnlink(path.join(CLAUDE_DIR, "hooks", f), counters);
311
325
  }
312
326
 
@@ -552,7 +566,7 @@ function cmdMigrate() {
552
566
 
553
567
  // Check PreToolUse hooks — ensure all critical hooks are present
554
568
  const requiredBashHooks = ["auto-update.js", "branch-guard.js", "pre-push.js", "pre-deploy-gate.js"];
555
- const requiredEditHooks = ["block-env-edit.js", "migration-guard.js"];
569
+ const requiredEditHooks = ["migration-guard.js"];
556
570
 
557
571
  if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
558
572
 
@@ -644,7 +658,7 @@ function cmdMigrate() {
644
658
  if (!settings.mcpServers["next-devtools"]) {
645
659
  settings.mcpServers["next-devtools"] = {
646
660
  command: "npx",
647
- args: ["next-devtools-mcp@0.3.10"],
661
+ args: ["next-devtools-mcp@latest"],
648
662
  disabled: false,
649
663
  };
650
664
  changes++;
@@ -755,6 +769,109 @@ function cmdAnalytics() {
755
769
  console.log("");
756
770
  }
757
771
 
772
+ // ─── ERP Ping ───────────────────────────────────────────
773
+ // Synthetic POST to ERP /api/v1/reports to verify connectivity, auth key
774
+ // validity, and endpoint health. Uses a distinct dry_run=true flag in the
775
+ // payload so receivers can filter these out of real report views.
776
+
777
+ function cmdErpPing() {
778
+ banner();
779
+ console.log("");
780
+
781
+ const cfg = readConfig();
782
+ const erpUrl = (cfg.erp && cfg.erp.url) || "https://portal.qualiasolutions.net";
783
+ const erpEnabled = !(cfg.erp && cfg.erp.enabled === false);
784
+ const keyFile = path.join(CLAUDE_DIR, ".erp-api-key");
785
+
786
+ console.log(` ${DIM}URL:${RESET} ${WHITE}${erpUrl}${RESET}`);
787
+ console.log(` ${DIM}Enabled:${RESET} ${erpEnabled ? `${GREEN}yes${RESET}` : `${YELLOW}no (erp.enabled=false)${RESET}`}`);
788
+
789
+ let apiKey = "";
790
+ try {
791
+ apiKey = fs.readFileSync(keyFile, "utf8").trim();
792
+ } catch {}
793
+ if (!apiKey) {
794
+ console.log(` ${DIM}Key:${RESET} ${RED}missing${RESET} ${DIM}(${keyFile})${RESET}`);
795
+ console.log("");
796
+ console.log(` ${RED}✗ Cannot ping — no API key. Ask Fawzi for one.${RESET}`);
797
+ console.log("");
798
+ process.exit(1);
799
+ }
800
+ console.log(` ${DIM}Key:${RESET} ${GREEN}present${RESET} ${DIM}(${apiKey.length} bytes)${RESET}`);
801
+ console.log("");
802
+
803
+ if (!erpEnabled) {
804
+ console.log(` ${YELLOW}ERP is disabled in config. Enable with:${RESET}`);
805
+ console.log(` ${DIM} qualia-framework erp-ping --enable${RESET}`);
806
+ console.log("");
807
+ process.exit(1);
808
+ }
809
+
810
+ const payload = JSON.stringify({
811
+ project: "qualia-framework-erp-ping",
812
+ project_id: "ping",
813
+ team_id: "qualia-solutions",
814
+ client_report_id: "QS-PING-00",
815
+ phase: 0,
816
+ phase_name: "ping",
817
+ status: "setup",
818
+ milestone: 0,
819
+ milestone_name: "ping",
820
+ submitted_by: cfg.installed_by || "ping",
821
+ submitted_at: new Date().toISOString(),
822
+ notes: "ERP PING — synthetic connectivity test, safe to ignore",
823
+ dry_run: true,
824
+ });
825
+
826
+ const started = Date.now();
827
+ const r = spawnSync("curl", [
828
+ "-sS", "-X", "POST",
829
+ "-H", `Authorization: Bearer ${apiKey}`,
830
+ "-H", "Content-Type: application/json",
831
+ "-d", payload,
832
+ "--max-time", "10",
833
+ "-w", "\n__HTTP__%{http_code}",
834
+ `${erpUrl}/api/v1/reports`,
835
+ ], { encoding: "utf8", timeout: 12000 });
836
+
837
+ const duration = Date.now() - started;
838
+ const raw = (r.stdout || "") + (r.stderr || "");
839
+ const httpMatch = raw.match(/__HTTP__(\d+)/);
840
+ const httpCode = httpMatch ? httpMatch[1] : "—";
841
+ const body = raw.replace(/\n?__HTTP__\d+/, "").trim();
842
+
843
+ console.log(` ${DIM}Response:${RESET} ${WHITE}HTTP ${httpCode}${RESET} ${DIM}(${duration}ms)${RESET}`);
844
+ if (body) {
845
+ try {
846
+ const j = JSON.parse(body);
847
+ if (j.ok && j.report_id) {
848
+ console.log(` ${DIM}report_id:${RESET} ${GREEN}${j.report_id}${RESET}`);
849
+ }
850
+ if (!j.ok && j.error) {
851
+ console.log(` ${DIM}error:${RESET} ${RED}${j.error}${RESET} ${DIM}${j.message || ""}${RESET}`);
852
+ }
853
+ } catch {
854
+ console.log(` ${DIM}body:${RESET} ${WHITE}${body.slice(0, 200)}${RESET}`);
855
+ }
856
+ }
857
+ console.log("");
858
+
859
+ if (httpCode === "200") {
860
+ console.log(` ${GREEN}✓ ERP reachable, key valid, endpoint healthy.${RESET}`);
861
+ console.log("");
862
+ process.exit(0);
863
+ }
864
+ if (httpCode === "401") {
865
+ console.log(` ${RED}✗ API key rejected. Ask Fawzi for a fresh key.${RESET}`);
866
+ } else if (httpCode === "—") {
867
+ console.log(` ${RED}✗ No response — DNS, TLS, or network issue.${RESET}`);
868
+ } else {
869
+ console.log(` ${YELLOW}! Unexpected response. Check ERP status.${RESET}`);
870
+ }
871
+ console.log("");
872
+ process.exit(1);
873
+ }
874
+
758
875
  function cmdHelp() {
759
876
  banner();
760
877
  console.log("");
@@ -767,6 +884,7 @@ function cmdHelp() {
767
884
  console.log(` qualia-framework ${TEAL}team${RESET} Manage team members (${DIM}list|add|remove${RESET})`);
768
885
  console.log(` qualia-framework ${TEAL}traces${RESET} View recent hook telemetry`);
769
886
  console.log(` qualia-framework ${TEAL}analytics${RESET} Show outcome scoring & gap cycle stats`);
887
+ console.log(` qualia-framework ${TEAL}erp-ping${RESET} Verify ERP connectivity + API key`);
770
888
  console.log("");
771
889
  console.log(` ${WHITE}After install:${RESET}`);
772
890
  console.log(` ${TG}/qualia${RESET} What should I do next?`);
@@ -819,6 +937,10 @@ switch (cmd) {
819
937
  case "stats":
820
938
  cmdAnalytics();
821
939
  break;
940
+ case "erp-ping":
941
+ case "ping":
942
+ cmdErpPing();
943
+ break;
822
944
  default:
823
945
  cmdHelp();
824
946
  }
package/bin/install.js CHANGED
@@ -517,7 +517,18 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
517
517
  ok(".erp-api-key (from $QUALIA_ERP_KEY)");
518
518
  } else if (fs.existsSync(erpKeyFile)) {
519
519
  try { fs.chmodSync(erpKeyFile, 0o600); } catch {}
520
- ok(".erp-api-key (existing — preserved)");
520
+ // Sanity check: warn on a clearly-empty/placeholder key. Genuine tokens
521
+ // from the ERP are ≥ 20 bytes; under 10 is almost certainly a mistake.
522
+ try {
523
+ const existingKey = fs.readFileSync(erpKeyFile, "utf8").trim();
524
+ if (existingKey.length < 10) {
525
+ warn(`.erp-api-key exists but looks truncated (${existingKey.length} bytes) — verify with 'qualia-framework erp-ping'`);
526
+ } else {
527
+ ok(".erp-api-key (existing — preserved)");
528
+ }
529
+ } catch {
530
+ ok(".erp-api-key (existing — preserved)");
531
+ }
521
532
  } else {
522
533
  // Disable ERP in the config we just wrote.
523
534
  try {
@@ -599,16 +610,23 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
599
610
  // bash/Git Bash requirement on Windows.
600
611
  const hd = path.join(CLAUDE_DIR, "hooks");
601
612
  const nodeCmd = (hookFile) => `node "${path.join(hd, hookFile)}"`;
602
- settings.hooks = {
613
+ const QUALIA_HOOK_SET = new Set([
614
+ "session-start.js", "auto-update.js", "branch-guard.js", "pre-push.js",
615
+ "pre-deploy-gate.js", "migration-guard.js", "pre-compact.js",
616
+ ]);
617
+ const isQualiaHookCmd = (cmd) => {
618
+ if (typeof cmd !== "string") return false;
619
+ for (const h of QUALIA_HOOK_SET) if (cmd.includes(h)) return true;
620
+ return false;
621
+ };
622
+
623
+ // Our canonical hook definitions, grouped per event/matcher.
624
+ const qualiaHooks = {
603
625
  SessionStart: [
604
626
  {
605
627
  matcher: ".*",
606
628
  hooks: [
607
- {
608
- type: "command",
609
- command: nodeCmd("session-start.js"),
610
- timeout: 5,
611
- },
629
+ { type: "command", command: nodeCmd("session-start.js"), timeout: 5 },
612
630
  ],
613
631
  },
614
632
  ],
@@ -616,44 +634,16 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
616
634
  {
617
635
  matcher: "Bash",
618
636
  hooks: [
619
- {
620
- type: "command",
621
- command: nodeCmd("auto-update.js"),
622
- timeout: 5,
623
- },
624
- {
625
- type: "command",
626
- if: "Bash(git push*)",
627
- command: nodeCmd("branch-guard.js"),
628
- timeout: 5,
629
- statusMessage: "⬢ Checking branch permissions...",
630
- },
631
- {
632
- type: "command",
633
- if: "Bash(git push*)",
634
- command: nodeCmd("pre-push.js"),
635
- timeout: 15,
636
- statusMessage: "⬢ Syncing tracking...",
637
- },
638
- {
639
- type: "command",
640
- if: "Bash(vercel --prod*)",
641
- command: nodeCmd("pre-deploy-gate.js"),
642
- timeout: 180,
643
- statusMessage: "⬢ Running quality gates...",
644
- },
637
+ { type: "command", command: nodeCmd("auto-update.js"), timeout: 5 },
638
+ { type: "command", if: "Bash(git push*)", command: nodeCmd("branch-guard.js"), timeout: 5, statusMessage: "⬢ Checking branch permissions..." },
639
+ { type: "command", if: "Bash(git push*)", command: nodeCmd("pre-push.js"), timeout: 15, statusMessage: "⬢ Syncing tracking..." },
640
+ { type: "command", if: "Bash(vercel --prod*)", command: nodeCmd("pre-deploy-gate.js"), timeout: 180, statusMessage: "⬢ Running quality gates..." },
645
641
  ],
646
642
  },
647
643
  {
648
644
  matcher: "Edit|Write",
649
645
  hooks: [
650
- {
651
- type: "command",
652
- if: "Edit(*migration*)|Write(*migration*)|Edit(*.sql)|Write(*.sql)",
653
- command: nodeCmd("migration-guard.js"),
654
- timeout: 10,
655
- statusMessage: "⬢ Checking migration safety...",
656
- },
646
+ { type: "command", if: "Edit(*migration*)|Write(*migration*)|Edit(*.sql)|Write(*.sql)", command: nodeCmd("migration-guard.js"), timeout: 10, statusMessage: "⬢ Checking migration safety..." },
657
647
  ],
658
648
  },
659
649
  ],
@@ -661,17 +651,27 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
661
651
  {
662
652
  matcher: "compact",
663
653
  hooks: [
664
- {
665
- type: "command",
666
- command: nodeCmd("pre-compact.js"),
667
- timeout: 15,
668
- statusMessage: "⬢ Saving state...",
669
- },
654
+ { type: "command", command: nodeCmd("pre-compact.js"), timeout: 15, statusMessage: "⬢ Saving state..." },
670
655
  ],
671
656
  },
672
657
  ],
673
658
  };
674
659
 
660
+ // Merge user hooks: strip Qualia-owned commands, preserve everything else.
661
+ if (!settings.hooks || typeof settings.hooks !== "object") settings.hooks = {};
662
+ for (const event of Object.keys(qualiaHooks)) {
663
+ const existing = Array.isArray(settings.hooks[event]) ? settings.hooks[event] : [];
664
+ // Remove Qualia-owned command entries from each matcher block, drop empty blocks.
665
+ const cleaned = [];
666
+ for (const block of existing) {
667
+ if (!block || !Array.isArray(block.hooks)) continue;
668
+ const kept = block.hooks.filter((h) => !isQualiaHookCmd(h && h.command));
669
+ if (kept.length > 0) cleaned.push({ ...block, hooks: kept });
670
+ }
671
+ // Append our canonical blocks after the preserved user ones.
672
+ settings.hooks[event] = [...cleaned, ...qualiaHooks[event]];
673
+ }
674
+
675
675
  // Permissions — no restrictions on env files or branches.
676
676
  // Everyone can read/write .env, push to main.
677
677
  if (!settings.permissions) settings.permissions = {};
@@ -684,7 +684,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
684
684
  if (!settings.mcpServers["next-devtools"]) {
685
685
  settings.mcpServers["next-devtools"] = {
686
686
  command: "npx",
687
- args: ["next-devtools-mcp@0.3.10"],
687
+ args: ["next-devtools-mcp@latest"],
688
688
  disabled: false,
689
689
  };
690
690
  ok("MCP: next-devtools (runtime error visibility for Next.js projects)");
package/bin/qualia-ui.js CHANGED
@@ -320,10 +320,10 @@ function cmdJourneyTree(journeyPath) {
320
320
 
321
321
  // Project name from frontmatter if present
322
322
  const projMatch = content.match(/^project:\s*"?(.+?)"?\s*$/m);
323
- const projectName = projMatch ? projMatch[1] : projectName();
323
+ const projName = projMatch ? projMatch[1] : projectName();
324
324
 
325
325
  console.log("");
326
- console.log(` ${TEAL}${BOLD}◯${RESET} ${WHITE}${BOLD}JOURNEY${RESET} ${DIM}▸${RESET} ${WHITE}${projectName}${RESET}`);
326
+ console.log(` ${TEAL}${BOLD}◯${RESET} ${WHITE}${BOLD}JOURNEY${RESET} ${DIM}▸${RESET} ${WHITE}${projName}${RESET}`);
327
327
  console.log(` ${RULE_DIM}`);
328
328
  console.log(` ${DIM}${milestones.length} milestones · currently at M${currentMilestone}${RESET}`);
329
329
  console.log("");