qualia-framework 2.6.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. package/CLAUDE.md +63 -0
  2. package/README.md +108 -30
  3. package/agents/builder.md +110 -0
  4. package/agents/planner.md +186 -0
  5. package/agents/qa-browser.md +186 -0
  6. package/agents/verifier.md +369 -0
  7. package/bin/cli.js +691 -492
  8. package/bin/install.js +622 -0
  9. package/bin/qualia-ui.js +284 -0
  10. package/bin/state.js +824 -0
  11. package/bin/statusline.js +252 -0
  12. package/docs/erp-contract.md +161 -0
  13. package/guide.md +63 -0
  14. package/hooks/auto-update.js +117 -0
  15. package/hooks/block-env-edit.js +52 -0
  16. package/hooks/branch-guard.js +68 -0
  17. package/hooks/migration-guard.js +83 -0
  18. package/hooks/pre-compact.js +52 -0
  19. package/hooks/pre-deploy-gate.js +149 -0
  20. package/hooks/pre-push.js +53 -0
  21. package/hooks/session-start.js +126 -0
  22. package/package.json +30 -20
  23. package/rules/design-reference.md +179 -0
  24. package/rules/frontend.md +126 -0
  25. package/rules/infrastructure.md +87 -0
  26. package/skills/qualia/SKILL.md +88 -0
  27. package/skills/qualia-build/SKILL.md +115 -0
  28. package/skills/qualia-debug/SKILL.md +87 -0
  29. package/skills/qualia-design/SKILL.md +99 -0
  30. package/skills/qualia-handoff/SKILL.md +66 -0
  31. package/skills/qualia-help/SKILL.md +60 -0
  32. package/skills/qualia-idk/SKILL.md +8 -0
  33. package/skills/qualia-learn/SKILL.md +111 -0
  34. package/skills/qualia-new/SKILL.md +323 -0
  35. package/skills/qualia-pause/SKILL.md +63 -0
  36. package/skills/qualia-plan/SKILL.md +101 -0
  37. package/skills/qualia-polish/SKILL.md +207 -0
  38. package/skills/qualia-quick/SKILL.md +37 -0
  39. package/skills/qualia-report/SKILL.md +114 -0
  40. package/skills/qualia-resume/SKILL.md +49 -0
  41. package/skills/qualia-review/SKILL.md +161 -0
  42. package/skills/qualia-ship/SKILL.md +90 -0
  43. package/skills/qualia-skill-new/SKILL.md +167 -0
  44. package/skills/qualia-task/SKILL.md +91 -0
  45. package/skills/qualia-test/SKILL.md +134 -0
  46. package/skills/qualia-verify/SKILL.md +113 -0
  47. package/templates/DESIGN.md +475 -0
  48. package/templates/help.html +476 -0
  49. package/templates/plan.md +42 -0
  50. package/templates/project.md +22 -0
  51. package/templates/state.md +27 -0
  52. package/templates/tracking.json +20 -0
  53. package/tests/bin.test.sh +687 -0
  54. package/tests/hooks.test.sh +384 -0
  55. package/tests/runner.js +1956 -0
  56. package/tests/state.test.sh +713 -0
  57. package/tests/statusline.test.sh +243 -0
  58. package/bin/collect-metrics.sh +0 -62
  59. package/framework/.claudeignore +0 -51
  60. package/framework/CLAUDE.md +0 -51
  61. package/framework/MCP_SETUP.md +0 -229
  62. package/framework/agents/architecture-strategist.md +0 -53
  63. package/framework/agents/backend-agent.md +0 -150
  64. package/framework/agents/code-simplicity-reviewer.md +0 -86
  65. package/framework/agents/frontend-agent.md +0 -111
  66. package/framework/agents/kieran-typescript-reviewer.md +0 -96
  67. package/framework/agents/performance-oracle.md +0 -111
  68. package/framework/agents/qualia-codebase-mapper.md +0 -761
  69. package/framework/agents/qualia-debugger.md +0 -1204
  70. package/framework/agents/qualia-executor.md +0 -882
  71. package/framework/agents/qualia-integration-checker.md +0 -424
  72. package/framework/agents/qualia-phase-researcher.md +0 -457
  73. package/framework/agents/qualia-plan-checker.md +0 -700
  74. package/framework/agents/qualia-planner.md +0 -1245
  75. package/framework/agents/qualia-project-researcher.md +0 -603
  76. package/framework/agents/qualia-research-synthesizer.md +0 -200
  77. package/framework/agents/qualia-roadmapper.md +0 -606
  78. package/framework/agents/qualia-verifier.md +0 -686
  79. package/framework/agents/red-team-qa.md +0 -130
  80. package/framework/agents/security-auditor.md +0 -72
  81. package/framework/agents/team-orchestrator.md +0 -229
  82. package/framework/agents/teams/framework-audit-team.md +0 -66
  83. package/framework/agents/teams/full-stack-team.md +0 -48
  84. package/framework/agents/teams/optimize-team.md +0 -53
  85. package/framework/agents/teams/review-team.md +0 -70
  86. package/framework/agents/teams/ship-team.md +0 -86
  87. package/framework/agents/test-agent.md +0 -182
  88. package/framework/hooks/auto-format.sh +0 -54
  89. package/framework/hooks/block-env-edit.sh +0 -42
  90. package/framework/hooks/branch-guard.sh +0 -43
  91. package/framework/hooks/confirm-delete.sh +0 -59
  92. package/framework/hooks/migration-validate.sh +0 -77
  93. package/framework/hooks/notification-speak.sh +0 -16
  94. package/framework/hooks/pre-commit.sh +0 -100
  95. package/framework/hooks/pre-compact.sh +0 -56
  96. package/framework/hooks/pre-deploy-gate.sh +0 -160
  97. package/framework/hooks/qualia-colors.sh +0 -32
  98. package/framework/hooks/retention-cleanup.sh +0 -62
  99. package/framework/hooks/save-session-state.sh +0 -185
  100. package/framework/hooks/session-context-loader.sh +0 -96
  101. package/framework/hooks/session-learn.sh +0 -32
  102. package/framework/hooks/skill-announce.sh +0 -123
  103. package/framework/hooks/tool-error-announce.sh +0 -27
  104. package/framework/install.ps1 +0 -323
  105. package/framework/install.sh +0 -313
  106. package/framework/qualia-framework/VERSION +0 -1
  107. package/framework/qualia-framework/assets/qualia-logo.png +0 -0
  108. package/framework/qualia-framework/bin/collect-metrics.sh +0 -67
  109. package/framework/qualia-framework/bin/generate-report-docx.py +0 -429
  110. package/framework/qualia-framework/bin/qualia-tools.js +0 -2201
  111. package/framework/qualia-framework/bin/qualia-tools.test.js +0 -1054
  112. package/framework/qualia-framework/references/checkpoints.md +0 -775
  113. package/framework/qualia-framework/references/completion-checklists.md +0 -359
  114. package/framework/qualia-framework/references/continuation-format.md +0 -249
  115. package/framework/qualia-framework/references/continuation-prompt.md +0 -97
  116. package/framework/qualia-framework/references/decimal-phase-calculation.md +0 -65
  117. package/framework/qualia-framework/references/design-quality.md +0 -56
  118. package/framework/qualia-framework/references/employee-guide.md +0 -167
  119. package/framework/qualia-framework/references/git-integration.md +0 -254
  120. package/framework/qualia-framework/references/git-planning-commit.md +0 -50
  121. package/framework/qualia-framework/references/model-profile-resolution.md +0 -32
  122. package/framework/qualia-framework/references/model-profiles.md +0 -73
  123. package/framework/qualia-framework/references/phase-argument-parsing.md +0 -61
  124. package/framework/qualia-framework/references/planning-config.md +0 -195
  125. package/framework/qualia-framework/references/questioning.md +0 -141
  126. package/framework/qualia-framework/references/tdd.md +0 -263
  127. package/framework/qualia-framework/references/ui-brand.md +0 -160
  128. package/framework/qualia-framework/references/verification-patterns.md +0 -612
  129. package/framework/qualia-framework/templates/DEBUG.md +0 -159
  130. package/framework/qualia-framework/templates/DESIGN.md +0 -81
  131. package/framework/qualia-framework/templates/UAT.md +0 -247
  132. package/framework/qualia-framework/templates/codebase/architecture.md +0 -255
  133. package/framework/qualia-framework/templates/codebase/concerns.md +0 -310
  134. package/framework/qualia-framework/templates/codebase/conventions.md +0 -307
  135. package/framework/qualia-framework/templates/codebase/integrations.md +0 -280
  136. package/framework/qualia-framework/templates/codebase/stack.md +0 -186
  137. package/framework/qualia-framework/templates/codebase/structure.md +0 -285
  138. package/framework/qualia-framework/templates/codebase/testing.md +0 -480
  139. package/framework/qualia-framework/templates/config.json +0 -35
  140. package/framework/qualia-framework/templates/context.md +0 -283
  141. package/framework/qualia-framework/templates/continue-here.md +0 -78
  142. package/framework/qualia-framework/templates/debug-subagent-prompt.md +0 -91
  143. package/framework/qualia-framework/templates/discovery.md +0 -146
  144. package/framework/qualia-framework/templates/lab-notes.md +0 -16
  145. package/framework/qualia-framework/templates/milestone-archive.md +0 -123
  146. package/framework/qualia-framework/templates/milestone.md +0 -115
  147. package/framework/qualia-framework/templates/phase-prompt.md +0 -567
  148. package/framework/qualia-framework/templates/planner-subagent-prompt.md +0 -117
  149. package/framework/qualia-framework/templates/project.md +0 -184
  150. package/framework/qualia-framework/templates/projects/ai-agent.md +0 -156
  151. package/framework/qualia-framework/templates/projects/mobile-app.md +0 -181
  152. package/framework/qualia-framework/templates/projects/voice-agent.md +0 -134
  153. package/framework/qualia-framework/templates/projects/website.md +0 -137
  154. package/framework/qualia-framework/templates/requirements.md +0 -231
  155. package/framework/qualia-framework/templates/research-project/ARCHITECTURE.md +0 -204
  156. package/framework/qualia-framework/templates/research-project/FEATURES.md +0 -147
  157. package/framework/qualia-framework/templates/research-project/PITFALLS.md +0 -200
  158. package/framework/qualia-framework/templates/research-project/STACK.md +0 -120
  159. package/framework/qualia-framework/templates/research-project/SUMMARY.md +0 -170
  160. package/framework/qualia-framework/templates/research.md +0 -552
  161. package/framework/qualia-framework/templates/roadmap.md +0 -206
  162. package/framework/qualia-framework/templates/state.md +0 -179
  163. package/framework/qualia-framework/templates/summary-complex.md +0 -59
  164. package/framework/qualia-framework/templates/summary-minimal.md +0 -41
  165. package/framework/qualia-framework/templates/summary-standard.md +0 -48
  166. package/framework/qualia-framework/templates/summary.md +0 -246
  167. package/framework/qualia-framework/templates/user-setup.md +0 -311
  168. package/framework/qualia-framework/templates/verification-report.md +0 -322
  169. package/framework/qualia-framework/workflows/add-phase.md +0 -179
  170. package/framework/qualia-framework/workflows/add-todo.md +0 -157
  171. package/framework/qualia-framework/workflows/audit-milestone.md +0 -241
  172. package/framework/qualia-framework/workflows/check-todos.md +0 -176
  173. package/framework/qualia-framework/workflows/complete-milestone.md +0 -858
  174. package/framework/qualia-framework/workflows/diagnose-issues.md +0 -219
  175. package/framework/qualia-framework/workflows/discovery-phase.md +0 -289
  176. package/framework/qualia-framework/workflows/discuss-phase.md +0 -534
  177. package/framework/qualia-framework/workflows/execute-phase.md +0 -559
  178. package/framework/qualia-framework/workflows/execute-plan.md +0 -438
  179. package/framework/qualia-framework/workflows/help.md +0 -470
  180. package/framework/qualia-framework/workflows/insert-phase.md +0 -220
  181. package/framework/qualia-framework/workflows/list-phase-assumptions.md +0 -178
  182. package/framework/qualia-framework/workflows/map-codebase.md +0 -327
  183. package/framework/qualia-framework/workflows/new-milestone.md +0 -363
  184. package/framework/qualia-framework/workflows/new-project.md +0 -982
  185. package/framework/qualia-framework/workflows/pause-work.md +0 -122
  186. package/framework/qualia-framework/workflows/plan-milestone-gaps.md +0 -256
  187. package/framework/qualia-framework/workflows/plan-phase.md +0 -422
  188. package/framework/qualia-framework/workflows/progress.md +0 -389
  189. package/framework/qualia-framework/workflows/quick.md +0 -252
  190. package/framework/qualia-framework/workflows/remove-phase.md +0 -326
  191. package/framework/qualia-framework/workflows/research-phase.md +0 -74
  192. package/framework/qualia-framework/workflows/resume-project.md +0 -306
  193. package/framework/qualia-framework/workflows/set-profile.md +0 -80
  194. package/framework/qualia-framework/workflows/settings.md +0 -145
  195. package/framework/qualia-framework/workflows/transition.md +0 -556
  196. package/framework/qualia-framework/workflows/update.md +0 -197
  197. package/framework/qualia-framework/workflows/verify-phase.md +0 -195
  198. package/framework/qualia-framework/workflows/verify-work.md +0 -625
  199. package/framework/rules/context7.md +0 -14
  200. package/framework/rules/frontend.md +0 -33
  201. package/framework/rules/speed.md +0 -23
  202. package/framework/scripts/__pycache__/say.cpython-314.pyc +0 -0
  203. package/framework/scripts/apply-retention.sh +0 -120
  204. package/framework/scripts/bootstrap-pop-os.sh +0 -354
  205. package/framework/scripts/claude-voice +0 -13
  206. package/framework/scripts/cleanup.sh +0 -131
  207. package/framework/scripts/cowork-mode.sh +0 -141
  208. package/framework/scripts/generate-project-claude-md.sh +0 -153
  209. package/framework/scripts/load-test-webhook.js +0 -172
  210. package/framework/scripts/say.py +0 -236
  211. package/framework/scripts/showcase-video-recorder/ffmpeg-builder.js +0 -167
  212. package/framework/scripts/showcase-video-recorder/playwright-helpers.js +0 -216
  213. package/framework/scripts/speak.py +0 -55
  214. package/framework/scripts/speak.sh +0 -18
  215. package/framework/scripts/status.sh +0 -138
  216. package/framework/scripts/sync-to-framework.sh +0 -65
  217. package/framework/scripts/voice-hotkey.py +0 -227
  218. package/framework/scripts/voice-input.sh +0 -51
  219. package/framework/skills/animate/SKILL.md +0 -202
  220. package/framework/skills/bolder/SKILL.md +0 -144
  221. package/framework/skills/browser-qa/SKILL.md +0 -536
  222. package/framework/skills/clarify/SKILL.md +0 -179
  223. package/framework/skills/client-handoff/SKILL.md +0 -135
  224. package/framework/skills/collab-onboard/SKILL.md +0 -111
  225. package/framework/skills/colorize/SKILL.md +0 -170
  226. package/framework/skills/critique/SKILL.md +0 -126
  227. package/framework/skills/deep-research/SKILL.md +0 -240
  228. package/framework/skills/delight/SKILL.md +0 -329
  229. package/framework/skills/deploy/SKILL.md +0 -261
  230. package/framework/skills/deploy-verify/SKILL.md +0 -377
  231. package/framework/skills/deploy-verify/scripts/canary-check.sh +0 -206
  232. package/framework/skills/deploy-verify/scripts/check-console-errors.js +0 -147
  233. package/framework/skills/deploy-verify/scripts/check-cwv.js +0 -139
  234. package/framework/skills/deploy-verify/scripts/project-detect.sh +0 -84
  235. package/framework/skills/deploy-verify/scripts/verify.sh +0 -548
  236. package/framework/skills/design-quieter/SKILL.md +0 -130
  237. package/framework/skills/distill/SKILL.md +0 -149
  238. package/framework/skills/docs-lookup/SKILL.md +0 -79
  239. package/framework/skills/fcm-notifications/SKILL.md +0 -125
  240. package/framework/skills/financial-ledger/SKILL.md +0 -1039
  241. package/framework/skills/frontend-master/NOTICE.md +0 -4
  242. package/framework/skills/frontend-master/SKILL.md +0 -127
  243. package/framework/skills/frontend-master/reference/color-and-contrast.md +0 -132
  244. package/framework/skills/frontend-master/reference/interaction-design.md +0 -123
  245. package/framework/skills/frontend-master/reference/motion-design.md +0 -99
  246. package/framework/skills/frontend-master/reference/responsive-design.md +0 -114
  247. package/framework/skills/frontend-master/reference/spatial-design.md +0 -100
  248. package/framework/skills/frontend-master/reference/typography.md +0 -131
  249. package/framework/skills/frontend-master/reference/ux-writing.md +0 -107
  250. package/framework/skills/harden/SKILL.md +0 -357
  251. package/framework/skills/i18n-rtl/SKILL.md +0 -752
  252. package/framework/skills/learn/SKILL.md +0 -95
  253. package/framework/skills/memory/SKILL.md +0 -50
  254. package/framework/skills/mobile-expo/SKILL.md +0 -977
  255. package/framework/skills/mobile-expo/references/store-checklist.md +0 -550
  256. package/framework/skills/nestjs-backend/README.md +0 -73
  257. package/framework/skills/nestjs-backend/SKILL.md +0 -446
  258. package/framework/skills/nestjs-backend/references/templates.md +0 -1173
  259. package/framework/skills/normalize/SKILL.md +0 -79
  260. package/framework/skills/onboard/SKILL.md +0 -242
  261. package/framework/skills/openrouter-agent/SKILL.md +0 -922
  262. package/framework/skills/polish/SKILL.md +0 -209
  263. package/framework/skills/pr/SKILL.md +0 -66
  264. package/framework/skills/qualia/SKILL.md +0 -199
  265. package/framework/skills/qualia-add-todo/SKILL.md +0 -68
  266. package/framework/skills/qualia-audit-milestone/SKILL.md +0 -95
  267. package/framework/skills/qualia-check-todos/SKILL.md +0 -55
  268. package/framework/skills/qualia-complete-milestone/SKILL.md +0 -134
  269. package/framework/skills/qualia-debug/SKILL.md +0 -149
  270. package/framework/skills/qualia-design/SKILL.md +0 -203
  271. package/framework/skills/qualia-discuss-phase/SKILL.md +0 -72
  272. package/framework/skills/qualia-evolve/SKILL.md +0 -200
  273. package/framework/skills/qualia-execute-phase/SKILL.md +0 -89
  274. package/framework/skills/qualia-framework-audit/SKILL.md +0 -604
  275. package/framework/skills/qualia-guide/SKILL.md +0 -32
  276. package/framework/skills/qualia-help/SKILL.md +0 -114
  277. package/framework/skills/qualia-idk/SKILL.md +0 -352
  278. package/framework/skills/qualia-list-phase-assumptions/SKILL.md +0 -67
  279. package/framework/skills/qualia-new-milestone/SKILL.md +0 -72
  280. package/framework/skills/qualia-new-project/SKILL.md +0 -232
  281. package/framework/skills/qualia-optimize/SKILL.md +0 -417
  282. package/framework/skills/qualia-pause-work/SKILL.md +0 -96
  283. package/framework/skills/qualia-plan-milestone-gaps/SKILL.md +0 -57
  284. package/framework/skills/qualia-plan-phase/SKILL.md +0 -104
  285. package/framework/skills/qualia-production-check/SKILL.md +0 -0
  286. package/framework/skills/qualia-progress/SKILL.md +0 -53
  287. package/framework/skills/qualia-quick/SKILL.md +0 -89
  288. package/framework/skills/qualia-report/SKILL.md +0 -166
  289. package/framework/skills/qualia-research-phase/SKILL.md +0 -88
  290. package/framework/skills/qualia-resume-work/SKILL.md +0 -62
  291. package/framework/skills/qualia-review/SKILL.md +0 -263
  292. package/framework/skills/qualia-start/SKILL.md +0 -161
  293. package/framework/skills/qualia-verify-work/SKILL.md +0 -132
  294. package/framework/skills/rag/SKILL.md +0 -750
  295. package/framework/skills/responsive/SKILL.md +0 -231
  296. package/framework/skills/retro/SKILL.md +0 -284
  297. package/framework/skills/sakani-conventions/SKILL.md +0 -136
  298. package/framework/skills/sakani-conventions/evals/evals.json +0 -23
  299. package/framework/skills/sakani-conventions/references/entities.md +0 -365
  300. package/framework/skills/sakani-conventions/references/error-codes.md +0 -95
  301. package/framework/skills/seo-master/SKILL.md +0 -490
  302. package/framework/skills/seo-master/references/checklist.md +0 -199
  303. package/framework/skills/seo-master/references/structured-data.md +0 -609
  304. package/framework/skills/ship/SKILL.md +0 -239
  305. package/framework/skills/stack-researcher/SKILL.md +0 -215
  306. package/framework/skills/status/SKILL.md +0 -154
  307. package/framework/skills/status/scripts/health-check.sh +0 -562
  308. package/framework/skills/subscription-payments/SKILL.md +0 -250
  309. package/framework/skills/supabase/SKILL.md +0 -973
  310. package/framework/skills/supabase/references/templates.md +0 -159
  311. package/framework/skills/team/SKILL.md +0 -67
  312. package/framework/skills/test-runner/SKILL.md +0 -202
  313. package/framework/skills/voice-agent/SKILL.md +0 -1312
  314. package/framework/skills/zoho-workflow/SKILL.md +0 -51
  315. package/framework/statusline-command.sh +0 -117
  316. package/framework/teams/default/inboxes/plan-04.json +0 -9
  317. package/framework/teams/review-team.md +0 -75
  318. package/framework/teams/ship-team.md +0 -86
  319. package/profiles/fawzi.json +0 -16
  320. package/profiles/hasan.json +0 -16
  321. package/profiles/moayad.json +0 -16
  322. package/templates/CLAUDE-owner.md +0 -52
  323. package/templates/CLAUDE.md.hbs +0 -58
  324. package/templates/env.claude.template +0 -12
  325. package/templates/settings.json +0 -172
  326. package/uninstall.sh +0 -90
  327. /package/{framework/rules → rules}/deployment.md +0 -0
  328. /package/{framework/rules → rules}/security.md +0 -0
package/bin/cli.js CHANGED
@@ -1,614 +1,813 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const fs = require('fs');
4
- const path = require('path');
5
- const readline = require('readline');
6
- const { execSync } = require('child_process');
7
-
8
- const CLAUDE_DIR = path.join(require('os').homedir(), '.claude');
9
- const PKG_ROOT = path.resolve(__dirname, '..');
10
- const FRAMEWORK_DIR = path.join(PKG_ROOT, 'framework');
11
- const PROFILES_DIR = path.join(PKG_ROOT, 'profiles');
12
- const TEMPLATES_DIR = path.join(PKG_ROOT, 'templates');
13
-
14
- // ── Colors ──
15
- const c = {
16
- reset: '\x1b[0m',
17
- bold: '\x1b[1m',
18
- dim: '\x1b[2m',
19
- green: '\x1b[32m',
20
- yellow: '\x1b[33m',
21
- red: '\x1b[31m',
22
- cyan: '\x1b[36m',
23
- magenta: '\x1b[35m',
24
- };
25
-
26
- function log(msg) { console.log(msg); }
27
- function ok(msg) { log(` ${c.green}\u2713${c.reset} ${msg}`); }
28
- function warn(msg) { log(` ${c.yellow}\u26A0${c.reset} ${msg}`); }
29
- function fail(msg) { log(` ${c.red}\u2717${c.reset} ${msg}`); }
30
- function step(n, total, msg) { log(`\n${c.cyan}[${n}/${total}]${c.reset} ${msg}`); }
31
-
32
- // ── Helpers ──
33
- function copyDirSync(src, dest) {
34
- fs.mkdirSync(dest, { recursive: true });
35
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
36
- const srcPath = path.join(src, entry.name);
37
- const destPath = path.join(dest, entry.name);
38
- if (entry.isDirectory()) {
39
- copyDirSync(srcPath, destPath);
40
- } else {
41
- fs.copyFileSync(srcPath, destPath);
42
- }
3
+ const { spawnSync } = require("child_process");
4
+ const path = require("path");
5
+ const fs = require("fs");
6
+ const readline = require("readline");
7
+
8
+ const TEAL = "\x1b[38;2;0;206;209m";
9
+ const TG = "\x1b[38;2;0;170;175m";
10
+ const DIM = "\x1b[38;2;80;90;100m";
11
+ const GREEN = "\x1b[38;2;52;211;153m";
12
+ const WHITE = "\x1b[38;2;220;225;230m";
13
+ const YELLOW = "\x1b[38;2;234;179;8m";
14
+ const RED = "\x1b[38;2;239;68;68m";
15
+ const RESET = "\x1b[0m";
16
+ const BOLD = "\x1b[1m";
17
+
18
+ const CLAUDE_DIR = path.join(require("os").homedir(), ".claude");
19
+ const PKG = require("../package.json");
20
+ const CONFIG_FILE = path.join(CLAUDE_DIR, ".qualia-config.json");
21
+
22
+ function readConfig() {
23
+ try {
24
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
25
+ } catch {
26
+ return {};
43
27
  }
44
28
  }
45
29
 
46
- function countDir(dir, type) {
47
- if (!fs.existsSync(dir)) return 0;
48
- const entries = fs.readdirSync(dir, { withFileTypes: true });
49
- if (type === 'dirs') return entries.filter(e => e.isDirectory()).length;
50
- return entries.filter(e => e.isFile()).length;
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");
51
33
  }
52
34
 
53
- function deepMerge(target, source) {
54
- const result = { ...target };
55
- for (const key of Object.keys(source)) {
56
- if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])
57
- && target[key] && typeof target[key] === 'object' && !Array.isArray(target[key])) {
58
- result[key] = deepMerge(target[key], source[key]);
59
- } else if (result[key] === undefined || result[key] === null || result[key] === '') {
60
- result[key] = source[key];
61
- }
62
- }
63
- return result;
35
+ function banner() {
36
+ console.log("");
37
+ console.log(` ${TEAL}${BOLD}⬢${RESET} ${WHITE}${BOLD}Qualia Framework${RESET} ${DIM}v${PKG.version}${RESET}`);
38
+ console.log(` ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
64
39
  }
65
40
 
66
- function loadProfiles() {
67
- const profiles = {};
68
- for (const file of fs.readdirSync(PROFILES_DIR)) {
69
- if (!file.endsWith('.json')) continue;
70
- const profile = JSON.parse(fs.readFileSync(path.join(PROFILES_DIR, file), 'utf8'));
71
- profiles[profile.code.toUpperCase()] = profile;
72
- }
73
- return profiles;
41
+ // ─── Commands ────────────────────────────────────────────
42
+
43
+ function cmdInstall() {
44
+ require("./install.js");
74
45
  }
75
46
 
76
- function renderCLAUDEmd(profile) {
77
- let template = fs.readFileSync(path.join(TEMPLATES_DIR, 'CLAUDE.md.hbs'), 'utf8');
47
+ function cmdVersion() {
48
+ banner();
49
+ const cfg = readConfig();
78
50
 
79
- const isOwner = profile.role === 'OWNER';
80
- const isDeveloper = profile.role === 'DEVELOPER';
81
- const isArabic = profile.language && profile.language.includes('ar');
51
+ console.log(` ${DIM}Installed:${RESET} ${WHITE}${PKG.version}${RESET}`);
52
+ if (cfg.installed_by) {
53
+ console.log(` ${DIM}User:${RESET} ${WHITE}${cfg.installed_by}${RESET} ${DIM}(${cfg.role})${RESET}`);
54
+ }
55
+ if (cfg.installed_at) {
56
+ console.log(` ${DIM}Date:${RESET} ${WHITE}${cfg.installed_at}${RESET}`);
57
+ }
82
58
 
83
- const roleTitle = isOwner ? 'Founder' : (profile.type === 'full-time' ? 'Full-time Developer' : 'Developer');
59
+ // Check for updates
60
+ try {
61
+ // spawnSync with argv — no bash-only `2>/dev/null` redirect, no shell
62
+ // interpolation. stdio: "ignore" on stderr silences any npm warnings
63
+ // (offline, proxy, etc.) without a shell redirect. shell: true on
64
+ // Windows because `npm` is a .cmd shim that only resolves through cmd.
65
+ const r = spawnSync("npm", ["view", "qualia-framework", "version"], {
66
+ stdio: ["ignore", "pipe", "ignore"],
67
+ shell: process.platform === "win32",
68
+ timeout: 5000,
69
+ encoding: "utf8",
70
+ });
71
+ const latest = (r.stdout || "").trim();
72
+ const semverGt = (a, b) => {
73
+ const pa = a.split(".").map(Number), pb = b.split(".").map(Number);
74
+ for (let i = 0; i < 3; i++) { if (pa[i] > pb[i]) return true; if (pa[i] < pb[i]) return false; }
75
+ return false;
76
+ };
77
+ if (latest && semverGt(latest, PKG.version)) {
78
+ console.log("");
79
+ console.log(` ${YELLOW}Update available:${RESET} ${WHITE}${latest}${RESET}`);
80
+ console.log(` ${DIM}Run:${RESET} npx qualia-framework update`);
81
+ } else if (latest) {
82
+ console.log(` ${DIM}Latest:${RESET} ${GREEN}${latest} ✓${RESET} ${DIM}(up to date)${RESET}`);
83
+ }
84
+ } catch {
85
+ console.log(` ${DIM}Latest:${RESET} ${DIM}(offline — couldn't check)${RESET}`);
86
+ }
87
+ console.log("");
88
+ }
84
89
 
85
- const replacements = {
86
- '{{fullName}}': profile.fullName || profile.name,
87
- '{{name}}': profile.name,
88
- '{{role}}': profile.role,
89
- '{{roleTitle}}': roleTitle,
90
- '{{location}}': profile.location || '',
91
- };
90
+ function cmdUpdate() {
91
+ banner();
92
+ const cfg = readConfig();
92
93
 
93
- for (const [key, val] of Object.entries(replacements)) {
94
- template = template.replace(new RegExp(escapeRegex(key), 'g'), val);
94
+ if (!cfg.code) {
95
+ console.log(` ${RED}✗${RESET} No install code saved. Run ${TEAL}install${RESET} first.`);
96
+ console.log("");
97
+ process.exit(1);
95
98
  }
96
99
 
97
- // Handle conditionals — inner first, then outer
98
- template = processConditional(template, 'canDeploy', profile.permissions?.canDeploy);
99
- template = processConditional(template, 'canAccessAllProjects', profile.permissions?.canAccessAllProjects);
100
- template = processConditional(template, 'isOwner', isOwner);
101
- template = processConditional(template, 'isDeveloper', isDeveloper);
102
- template = processConditional(template, 'isArabic', isArabic);
100
+ console.log(` ${DIM}Current:${RESET} ${WHITE}${PKG.version}${RESET}`);
101
+ console.log(` ${DIM}Updating...${RESET}`);
102
+ console.log("");
103
103
 
104
- // Clean up empty lines
105
- template = template.replace(/\n{3,}/g, '\n\n');
104
+ try {
105
+ const r = spawnSync("npx", ["qualia-framework@latest", "install"], {
106
+ input: cfg.code + "\n",
107
+ stdio: ["pipe", "inherit", "inherit"],
108
+ shell: process.platform === "win32", // npx is a .cmd shim on Windows — must go through shell
109
+ timeout: 120000,
110
+ encoding: "utf8",
111
+ });
112
+ if (r.status !== 0) {
113
+ console.log(` ${RED}✗${RESET} Update failed. Run manually: npx qualia-framework@latest install`);
114
+ process.exit(1);
115
+ }
116
+ } catch (e) {
117
+ console.log(` ${RED}✗${RESET} Update failed: ${e.message}`);
118
+ console.log(` ${DIM}Run manually:${RESET} npx qualia-framework@latest install`);
119
+ process.exit(1);
120
+ }
121
+ }
106
122
 
107
- return template;
123
+ // ─── Uninstall ───────────────────────────────────────────
124
+ // Surgical removal of the Qualia Framework from ~/.claude/.
125
+ // Preserves CLAUDE.md (user may have customized it) and preserves any
126
+ // non-Qualia entries in settings.json (other hooks, user env vars, etc.).
127
+ // --yes / -y skips the confirmation prompt for scripted use.
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.
131
+ const QUALIA_HOOK_FILES = [
132
+ "session-start.js",
133
+ "auto-update.js",
134
+ "branch-guard.js",
135
+ "pre-push.js",
136
+ "block-env-edit.js",
137
+ "migration-guard.js",
138
+ "pre-deploy-gate.js",
139
+ "pre-compact.js",
140
+ ];
141
+
142
+ // 4 Qualia agents — only these are removed.
143
+ const QUALIA_AGENT_FILES = ["planner.md", "builder.md", "verifier.md", "qa-browser.md"];
144
+
145
+ // 3 Qualia bin scripts.
146
+ const QUALIA_BIN_FILES = ["state.js", "qualia-ui.js", "statusline.js"];
147
+
148
+ // 5 Qualia rules.
149
+ const QUALIA_RULE_FILES = ["security.md", "frontend.md", "design-reference.md", "deployment.md", "infrastructure.md"];
150
+
151
+ function promptYesNo(question, defaultYes) {
152
+ return new Promise((resolve) => {
153
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
154
+ const suffix = defaultYes ? " (Y/n)" : " (y/N)";
155
+ rl.question(` ${WHITE}${question}${RESET}${suffix} `, (answer) => {
156
+ rl.close();
157
+ const a = String(answer || "").trim().toLowerCase();
158
+ if (!a) return resolve(defaultYes);
159
+ resolve(a === "y" || a === "yes");
160
+ });
161
+ });
108
162
  }
109
163
 
110
- function escapeRegex(str) {
111
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
164
+ function safeUnlink(p, counters) {
165
+ try {
166
+ if (fs.existsSync(p)) {
167
+ fs.unlinkSync(p);
168
+ counters.filesRemoved++;
169
+ }
170
+ } catch (e) {
171
+ counters.errors.push(`${p}: ${e.message}`);
172
+ }
112
173
  }
113
174
 
114
- function processConditional(text, name, value) {
115
- const ifRegex = new RegExp(`\\{\\{#if ${name}\\}\\}([\\s\\S]*?)(?:\\{\\{else\\}\\}([\\s\\S]*?))?\\{\\{/if\\}\\}`, 'g');
116
- return text.replace(ifRegex, (_, ifBlock, elseBlock) => {
117
- return value ? ifBlock : (elseBlock || '');
118
- });
175
+ function safeRmDir(p, counters) {
176
+ try {
177
+ if (fs.existsSync(p)) {
178
+ fs.rmSync(p, { recursive: true, force: true });
179
+ counters.dirsRemoved++;
180
+ }
181
+ } catch (e) {
182
+ counters.errors.push(`${p}: ${e.message}`);
183
+ }
119
184
  }
120
185
 
121
- function mergeSettings(existingPath, templatePath) {
122
- const template = JSON.parse(fs.readFileSync(templatePath, 'utf8'));
186
+ function cleanSettingsJson(counters) {
187
+ const settingsPath = path.join(CLAUDE_DIR, "settings.json");
188
+ if (!fs.existsSync(settingsPath)) return;
189
+ let settings;
190
+ try {
191
+ settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
192
+ } catch (e) {
193
+ counters.errors.push(`settings.json: ${e.message}`);
194
+ return;
195
+ }
123
196
 
124
- if (!fs.existsSync(existingPath)) return template;
197
+ // Only remove entries that point at qualia paths. Leave everything else.
198
+ const isQualiaCommand = (cmd) =>
199
+ typeof cmd === "string" && (cmd.includes("qualia") || cmd.includes(".claude/hooks/") || cmd.includes(".claude/bin/"));
125
200
 
126
- const existing = JSON.parse(fs.readFileSync(existingPath, 'utf8'));
201
+ const filterHookArray = (arr) => {
202
+ if (!Array.isArray(arr)) return arr;
203
+ return arr
204
+ .map((entry) => {
205
+ if (!entry || !Array.isArray(entry.hooks)) return entry;
206
+ const hooks = entry.hooks.filter((h) => !isQualiaCommand(h && h.command));
207
+ return { ...entry, hooks };
208
+ })
209
+ .filter((entry) => Array.isArray(entry.hooks) && entry.hooks.length > 0);
210
+ };
127
211
 
128
- // Preserve existing env values (API keys), merge in new ones
129
- const merged = { ...template };
130
- if (existing.env) {
131
- merged.env = { ...template.env };
132
- for (const [key, val] of Object.entries(existing.env)) {
133
- if (val && val !== '' && !val.startsWith('$')) {
134
- merged.env[key] = val;
212
+ 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
+ }
135
221
  }
136
222
  }
223
+ // If hooks is now empty, remove it entirely.
224
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
137
225
  }
138
226
 
139
- // Overwrite hooks, permissions, statusLine from template (framework-managed)
140
- merged.hooks = template.hooks;
141
- merged.permissions = template.permissions;
142
- // Use node-based statusline on Windows (no bash), bash on Unix
143
- if (process.platform === 'win32') {
144
- merged.statusLine = { type: 'command', command: 'node ~/.claude/statusline-command.js' };
145
- } else {
146
- merged.statusLine = template.statusLine;
227
+ // Status line only drop it if it points at our renderer.
228
+ if (settings.statusLine && typeof settings.statusLine === "object") {
229
+ const cmd = settings.statusLine.command || "";
230
+ if (isQualiaCommand(cmd) || cmd.includes("statusline.js") || cmd.includes("qualia-ui")) {
231
+ delete settings.statusLine;
232
+ }
147
233
  }
148
234
 
149
- // Preserve user's existing plugins and MCP servers
150
- if (existing.enabledPlugins) {
151
- merged.enabledPlugins = existing.enabledPlugins;
152
- }
153
- if (existing.mcpServers) {
154
- merged.mcpServers = existing.mcpServers;
155
- }
235
+ // Qualia-specific spinner overrides.
236
+ if (settings.spinnerVerbs) delete settings.spinnerVerbs;
237
+ if (settings.spinnerTipsOverride) delete settings.spinnerTipsOverride;
156
238
 
157
- return merged;
158
- }
239
+ // Leave settings.env alone — the user may have other env vars in there.
159
240
 
160
- function prompt(question) {
161
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
162
- return new Promise(resolve => {
163
- rl.question(question, answer => {
164
- rl.close();
165
- resolve(answer.trim());
166
- });
167
- });
241
+ try {
242
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
243
+ counters.settingsCleaned = true;
244
+ } catch (e) {
245
+ counters.errors.push(`settings.json write: ${e.message}`);
246
+ }
168
247
  }
169
248
 
170
- // ── Uninstall ──
171
- async function runUninstall() {
172
- log('');
173
- log(`${c.red}\u2554${'═'.repeat(54)}\u2557${c.reset}`);
174
- log(`${c.red}\u2551${c.reset}${c.bold} QUALIA FRAMEWORK \u2014 UNINSTALLER ${c.reset}${c.red}\u2551${c.reset}`);
175
- log(`${c.red}\u255A${'═'.repeat(54)}\u255D${c.reset}`);
176
- log('');
249
+ async function cmdUninstall() {
250
+ banner();
177
251
 
178
- if (!fs.existsSync(CLAUDE_DIR)) {
179
- warn('Nothing to uninstall \u2014 ~/.claude/ does not exist.');
180
- return;
252
+ const args = process.argv.slice(3);
253
+ const skipConfirm = args.includes("-y") || args.includes("--yes");
254
+
255
+ const cfg = readConfig();
256
+ console.log("");
257
+ if (cfg.installed_by) {
258
+ console.log(` ${DIM}User:${RESET} ${WHITE}${cfg.installed_by}${RESET} ${DIM}(${cfg.role || "?"})${RESET}`);
259
+ } else {
260
+ console.log(` ${DIM}No Qualia config found at${RESET} ${WHITE}${CONFIG_FILE}${RESET}`);
181
261
  }
262
+ console.log("");
182
263
 
183
- // Count everything in ~/.claude/
184
- const totalFiles = countFilesRecursive(CLAUDE_DIR);
185
-
186
- log(`${c.bold} NUCLEAR UNINSTALL \u2014 deleting EVERYTHING in ~/.claude/${c.reset}`);
187
- log('');
188
- log(` ${c.red}\u2022${c.reset} ${totalFiles} files will be permanently deleted`);
189
- log(` ${c.red}\u2022${c.reset} settings.json will be reset to {}`);
190
- log(` ${c.red}\u2022${c.reset} All skills, agents, hooks, rules, knowledge \u2014 gone`);
191
- log(` ${c.red}\u2022${c.reset} All memory, projects, plans, cache \u2014 gone`);
192
- log(` ${c.red}\u2022${c.reset} CLAUDE.md, .env.claude, all config \u2014 gone`);
193
- log('');
194
-
195
- // Confirmation
196
- const force = process.argv.includes('--force');
197
- if (!force) {
198
- log(`${c.red} \u26A0 THIS CANNOT BE UNDONE.${c.reset}`);
199
- log(`${c.red} \u26A0 ~/.claude/ will be wiped clean. Nothing survives.${c.reset}`);
200
- log('');
201
- const answer = await prompt(` Type 'UNINSTALL' to confirm: `);
202
- if (answer !== 'UNINSTALL') {
203
- log('');
204
- ok('Cancelled. Nothing was deleted.');
264
+ if (!skipConfirm) {
265
+ const confirm = await promptYesNo("Are you sure you want to uninstall the Qualia Framework?", false);
266
+ if (!confirm) {
267
+ console.log("");
268
+ console.log(` ${DIM}Aborted.${RESET}`);
269
+ console.log("");
205
270
  return;
206
271
  }
207
- log('');
208
272
  }
209
273
 
210
- // Step 1: Nuke everything in ~/.claude/ except settings.json (we handle that separately)
211
- step(1, 2, 'Nuking ~/.claude/ ...');
274
+ // Preserve knowledge base by default.
275
+ let preserveKnowledge = true;
276
+ if (!skipConfirm) {
277
+ preserveKnowledge = await promptYesNo(
278
+ "Preserve knowledge base? (your learned patterns, fixes, client prefs)",
279
+ true
280
+ );
281
+ }
212
282
 
213
- for (const entry of fs.readdirSync(CLAUDE_DIR, { withFileTypes: true })) {
214
- const p = path.join(CLAUDE_DIR, entry.name);
283
+ console.log("");
284
+ console.log(` ${DIM}Removing framework files...${RESET}`);
285
+ console.log("");
215
286
 
216
- // Skip settings.json we reset it in step 2
217
- if (entry.name === 'settings.json') continue;
287
+ const counters = { filesRemoved: 0, dirsRemoved: 0, settingsCleaned: false, errors: [] };
218
288
 
219
- try {
220
- if (entry.isDirectory()) {
221
- fs.rmSync(p, { recursive: true, force: true });
222
- ok(`${entry.name}/`);
223
- } else if (isSymlink(p)) {
224
- fs.unlinkSync(p);
225
- ok(`${entry.name} (symlink)`);
226
- } else {
227
- fs.unlinkSync(p);
228
- ok(entry.name);
289
+ // Skills — any directory starting with "qualia" under ~/.claude/skills/.
290
+ const skillsDir = path.join(CLAUDE_DIR, "skills");
291
+ try {
292
+ if (fs.existsSync(skillsDir)) {
293
+ for (const name of fs.readdirSync(skillsDir)) {
294
+ if (name === "qualia" || name.startsWith("qualia-")) {
295
+ safeRmDir(path.join(skillsDir, name), counters);
296
+ }
229
297
  }
230
- } catch (e) {
231
- fail(`${entry.name}: ${e.message}`);
232
298
  }
299
+ } catch (e) {
300
+ counters.errors.push(`skills scan: ${e.message}`);
233
301
  }
234
302
 
235
- // Step 2: Reset settings.json to empty
236
- step(2, 2, 'Resetting settings.json ...');
237
- const settingsPath = path.join(CLAUDE_DIR, 'settings.json');
238
- if (fs.existsSync(settingsPath)) {
239
- fs.writeFileSync(settingsPath, '{}\n');
240
- ok('settings.json reset to {}');
241
- } else {
242
- ok('No settings.json found');
303
+ // Agents only the 4 Qualia ones.
304
+ for (const f of QUALIA_AGENT_FILES) {
305
+ safeUnlink(path.join(CLAUDE_DIR, "agents", f), counters);
243
306
  }
244
307
 
245
- log('');
246
- log(`${c.green}\u2554${'═'.repeat(54)}\u2557${c.reset}`);
247
- log(`${c.green}\u2551${c.reset}${c.bold} \u2713 NUKED \u2014 ~/.claude/ is clean ${c.reset}${c.green}\u2551${c.reset}`);
248
- log(`${c.green}\u255A${'═'.repeat(54)}\u255D${c.reset}`);
249
- log('');
250
- log(` ${c.dim}Claude Code will use default behavior on next launch.${c.reset}`);
251
- log('');
252
- }
253
-
254
- function isSymlink(p) {
255
- try { fs.lstatSync(p); return fs.lstatSync(p).isSymbolicLink(); } catch { return false; }
256
- }
308
+ // Hooks — only the 8 Qualia ones.
309
+ for (const f of QUALIA_HOOK_FILES) {
310
+ safeUnlink(path.join(CLAUDE_DIR, "hooks", f), counters);
311
+ }
257
312
 
258
- // ── Main ──
259
- async function main() {
260
- const command = process.argv[2];
313
+ // Bin scripts — only the 3 Qualia ones.
314
+ for (const f of QUALIA_BIN_FILES) {
315
+ safeUnlink(path.join(CLAUDE_DIR, "bin", f), counters);
316
+ }
261
317
 
262
- if (command === 'update') {
263
- await runUpdate();
264
- } else if (command === 'verify') {
265
- runVerify();
266
- } else if (command === 'uninstall') {
267
- await runUninstall();
268
- } else {
269
- await runInstall();
318
+ // Rules all 4.
319
+ for (const f of QUALIA_RULE_FILES) {
320
+ safeUnlink(path.join(CLAUDE_DIR, "rules", f), counters);
270
321
  }
271
- }
272
322
 
273
- async function runInstall() {
274
- log('');
275
- log(`${c.cyan}\u2554${'═'.repeat(54)}\u2557${c.reset}`);
276
- log(`${c.cyan}\u2551${c.reset}${c.bold} QUALIA FRAMEWORK \u2014 INSTALLER ${c.reset}${c.cyan}\u2551${c.reset}`);
277
- log(`${c.cyan}\u255A${'═'.repeat(54)}\u255D${c.reset}`);
278
- log('');
279
-
280
- const profiles = loadProfiles();
281
- const code = await prompt(` Enter your employee code: `);
282
- const profile = profiles[code.toUpperCase()];
283
-
284
- if (!profile) {
285
- log('');
286
- fail(`Unknown code "${code}". Contact Fawzi for your employee code.`);
287
- log(`${c.dim} Available format: QS-NAME-YEAR${c.reset}`);
288
- process.exit(1);
323
+ // Templates directory (entire).
324
+ safeRmDir(path.join(CLAUDE_DIR, "qualia-templates"), counters);
325
+
326
+ // Knowledge directory (optional preservation).
327
+ if (!preserveKnowledge) {
328
+ safeRmDir(path.join(CLAUDE_DIR, "knowledge"), counters);
289
329
  }
290
330
 
291
- log('');
292
- log(` ${c.green}\u2192${c.reset} Welcome, ${c.bold}${profile.name}${c.reset}! Setting up your Qualia environment...`);
331
+ // Config + state files.
332
+ safeUnlink(path.join(CLAUDE_DIR, ".qualia-config.json"), counters);
333
+ safeUnlink(path.join(CLAUDE_DIR, ".qualia-last-update-check"), counters);
334
+ safeUnlink(path.join(CLAUDE_DIR, ".erp-api-key"), counters);
335
+ safeUnlink(path.join(CLAUDE_DIR, ".qualia-team.json"), counters);
336
+ safeUnlink(path.join(CLAUDE_DIR, "qualia-guide.md"), counters);
293
337
 
294
- const TOTAL = 6;
338
+ // Traces directory.
339
+ safeRmDir(path.join(CLAUDE_DIR, ".qualia-traces"), counters);
295
340
 
296
- // Step 1: Backup existing
297
- step(1, TOTAL, 'Checking existing ~/.claude/ ...');
298
- if (fs.existsSync(CLAUDE_DIR)) {
299
- const date = new Date().toISOString().split('T')[0];
300
- const backupDir = `${CLAUDE_DIR}.backup-${date}`;
301
- if (!fs.existsSync(backupDir)) {
302
- // Don't backup the whole thing — just the config files we'll overwrite
303
- fs.mkdirSync(backupDir, { recursive: true });
304
- for (const f of ['CLAUDE.md', 'settings.json', '.env.claude']) {
305
- const src = path.join(CLAUDE_DIR, f);
306
- if (fs.existsSync(src)) {
307
- fs.copyFileSync(src, path.join(backupDir, f));
308
- }
309
- }
310
- ok(`Config backed up to ${c.dim}~/.claude.backup-${date}/${c.reset}`);
311
- } else {
312
- ok('Backup already exists for today');
313
- }
341
+ // Clean settings.json surgically.
342
+ cleanSettingsJson(counters);
343
+
344
+ // Summary.
345
+ console.log("");
346
+ console.log(`${TEAL} ⬢ Uninstall complete${RESET}`);
347
+ console.log(`${DIM} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
348
+ console.log(` ${DIM}Files removed:${RESET} ${WHITE}${counters.filesRemoved}${RESET}`);
349
+ console.log(` ${DIM}Directories removed:${RESET} ${WHITE}${counters.dirsRemoved}${RESET}`);
350
+ console.log(
351
+ ` ${DIM}settings.json:${RESET} ${counters.settingsCleaned ? `${GREEN}cleaned ✓${RESET}` : `${DIM}not present${RESET}`}`
352
+ );
353
+ if (preserveKnowledge) {
354
+ console.log(` ${DIM}Knowledge base:${RESET} ${GREEN}preserved ✓${RESET}`);
314
355
  } else {
315
- fs.mkdirSync(CLAUDE_DIR, { recursive: true });
316
- ok('Created ~/.claude/');
356
+ console.log(` ${DIM}Knowledge base:${RESET} ${YELLOW}removed${RESET}`);
317
357
  }
318
358
 
319
- // Step 2: Copy framework files
320
- step(2, TOTAL, 'Installing framework files ...');
321
-
322
- const dirs = ['skills', 'hooks', 'agents', 'rules', 'qualia-framework', 'scripts', 'knowledge'];
323
- for (const dir of dirs) {
324
- const src = path.join(FRAMEWORK_DIR, dir);
325
- const dest = path.join(CLAUDE_DIR, dir);
326
- if (fs.existsSync(src)) {
327
- copyDirSync(src, dest);
359
+ if (counters.errors.length > 0) {
360
+ console.log("");
361
+ console.log(` ${YELLOW}${counters.errors.length} warning(s):${RESET}`);
362
+ for (const err of counters.errors.slice(0, 5)) {
363
+ console.log(` ${DIM}${err}${RESET}`);
328
364
  }
329
365
  }
330
366
 
331
- // Copy standalone files (both .sh and .js for cross-platform)
332
- for (const f of ['statusline-command.sh', 'statusline-command.js', 'askpass.sh']) {
333
- const src = path.join(FRAMEWORK_DIR, f);
334
- if (fs.existsSync(src)) {
335
- fs.copyFileSync(src, path.join(CLAUDE_DIR, f));
367
+ console.log("");
368
+ console.log(
369
+ ` ${YELLOW}Manual step:${RESET} edit ${WHITE}~/.claude/CLAUDE.md${RESET} to remove the Qualia Framework section if desired.`
370
+ );
371
+ console.log("");
372
+ }
373
+
374
+ // ─── Team Management ────────────────────────────────────
375
+ // External team file at ~/.claude/.qualia-team.json.
376
+ // Falls back to embedded defaults in install.js.
377
+
378
+ function getDefaultTeam() {
379
+ return {
380
+ "QS-FAWZI-01": { name: "Fawzi Goussous", role: "OWNER", description: "Company owner. Full access. Can push to main, approve deploys, edit secrets." },
381
+ "QS-HASAN-02": { name: "Hasan", role: "EMPLOYEE", description: "Developer. Feature branches only. Cannot push to main or edit .env files." },
382
+ "QS-MOAYAD-03": { name: "Moayad", role: "EMPLOYEE", description: "Developer. Feature branches only. Cannot push to main or edit .env files." },
383
+ "QS-RAMA-04": { name: "Rama", role: "EMPLOYEE", description: "Developer. Feature branches only. Cannot push to main or edit .env files." },
384
+ "QS-SALLY-05": { name: "Sally", role: "EMPLOYEE", description: "Developer. Feature branches only. Cannot push to main or edit .env files." },
385
+ };
386
+ }
387
+
388
+ function readTeamFile() {
389
+ const teamFile = path.join(CLAUDE_DIR, ".qualia-team.json");
390
+ try {
391
+ if (fs.existsSync(teamFile)) {
392
+ const data = JSON.parse(fs.readFileSync(teamFile, "utf8"));
393
+ if (data && typeof data === "object" && Object.keys(data).length > 0) return data;
336
394
  }
395
+ } catch {}
396
+ return null;
397
+ }
398
+
399
+ function writeTeamFile(team) {
400
+ if (!fs.existsSync(CLAUDE_DIR)) fs.mkdirSync(CLAUDE_DIR, { recursive: true });
401
+ fs.writeFileSync(path.join(CLAUDE_DIR, ".qualia-team.json"), JSON.stringify(team, null, 2) + "\n");
402
+ }
403
+
404
+ function parseTeamArgs(argv) {
405
+ const args = {};
406
+ for (let i = 0; i < argv.length; i++) {
407
+ if (argv[i] === "--code" && argv[i + 1]) { args.code = argv[++i]; }
408
+ else if (argv[i] === "--name" && argv[i + 1]) { args.name = argv[++i]; }
409
+ else if (argv[i] === "--role" && argv[i + 1]) { args.role = argv[++i].toUpperCase(); }
337
410
  }
411
+ return args;
412
+ }
338
413
 
339
- // Make hooks executable
340
- const hooksDir = path.join(CLAUDE_DIR, 'hooks');
341
- if (fs.existsSync(hooksDir)) {
342
- for (const f of fs.readdirSync(hooksDir)) {
343
- if (f.endsWith('.sh')) {
344
- fs.chmodSync(path.join(hooksDir, f), 0o755);
414
+ function cmdTeam() {
415
+ const sub = process.argv[3];
416
+
417
+ switch (sub) {
418
+ case "list": {
419
+ banner();
420
+ console.log("");
421
+ const team = readTeamFile();
422
+ const source = team || getDefaultTeam();
423
+ const label = team ? "team file" : "embedded defaults";
424
+ console.log(` ${DIM}Source: ${label}${RESET}`);
425
+ console.log("");
426
+ for (const [code, member] of Object.entries(source)) {
427
+ const roleColor = member.role === "OWNER" ? TEAL : WHITE;
428
+ console.log(` ${WHITE}${code}${RESET} ${roleColor}${member.role}${RESET} ${DIM}${member.name}${RESET}`);
345
429
  }
430
+ console.log("");
431
+ break;
346
432
  }
347
- }
348
- // Make standalone scripts executable
349
- for (const f of ['statusline-command.sh', 'statusline-command.js', 'askpass.sh']) {
350
- const p = path.join(CLAUDE_DIR, f);
351
- if (fs.existsSync(p)) try { fs.chmodSync(p, 0o755); } catch {}
352
- }
353
- // Make qualia-framework bin executable
354
- const engineBin = path.join(CLAUDE_DIR, 'qualia-framework', 'bin');
355
- if (fs.existsSync(engineBin)) {
356
- for (const f of fs.readdirSync(engineBin)) {
357
- const p = path.join(engineBin, f);
358
- try { fs.chmodSync(p, 0o755); } catch {}
433
+
434
+ case "add": {
435
+ const args = parseTeamArgs(process.argv.slice(4));
436
+ if (!args.code || !args.name) {
437
+ console.log(` ${RED}Usage:${RESET} qualia-framework team add --code QS-NAME-NN --name "Full Name" [--role EMPLOYEE|OWNER]`);
438
+ process.exit(1);
439
+ }
440
+ const team = readTeamFile() || getDefaultTeam();
441
+ const code = args.code.toUpperCase();
442
+ const role = args.role || "EMPLOYEE";
443
+ team[code] = {
444
+ name: args.name,
445
+ role,
446
+ description: role === "OWNER"
447
+ ? "Company owner. Full access. Can push to main, approve deploys, edit secrets."
448
+ : "Developer. Feature branches only. Cannot push to main or edit .env files.",
449
+ };
450
+ writeTeamFile(team);
451
+ console.log(` ${GREEN}+${RESET} ${WHITE}${code}${RESET} ${DIM}(${args.name}, ${role})${RESET}`);
452
+ break;
359
453
  }
360
- }
361
454
 
362
- ok(`${countDir(path.join(CLAUDE_DIR, 'skills'), 'dirs')} skills`);
363
- ok(`${countDir(hooksDir, 'files')} hooks (chmod +x)`);
364
- ok(`${countDir(path.join(CLAUDE_DIR, 'agents'), 'files')} agents`);
365
- ok(`${countDir(path.join(CLAUDE_DIR, 'rules'), 'files')} rule files`);
366
- ok('qualia-framework');
367
- ok('knowledge base');
368
-
369
- // Step 3: Generate CLAUDE.md
370
- step(3, TOTAL, 'Generating your CLAUDE.md ...');
371
- const claudeMd = renderCLAUDEmd(profile);
372
- fs.writeFileSync(path.join(CLAUDE_DIR, 'CLAUDE.md'), claudeMd);
373
-
374
- const roleDesc = profile.role === 'OWNER' ? 'Full authority' :
375
- `Feature branches${profile.permissions?.canDeploy ? ', can deploy' : ', no prod deploy'}`;
376
- ok(`Identity: ${profile.name} \u2014 ${profile.role === 'OWNER' ? 'Founder' : 'Developer'} at Qualia Solutions`);
377
- ok(`Role: ${profile.role} (${roleDesc})`);
378
-
379
- // Step 4: Configure settings.json
380
- step(4, TOTAL, 'Configuring settings.json ...');
381
- const settingsPath = path.join(CLAUDE_DIR, 'settings.json');
382
- const templateSettingsPath = path.join(TEMPLATES_DIR, 'settings.json');
383
- const mergedSettings = mergeSettings(settingsPath, templateSettingsPath);
384
-
385
- // Fix SUDO_ASKPASS to absolute path
386
- mergedSettings.env.SUDO_ASKPASS = path.join(CLAUDE_DIR, 'askpass.sh');
387
- mergedSettings.env.CLAUDE_ENV_FILE = path.join(CLAUDE_DIR, '.env.claude');
388
-
389
- fs.writeFileSync(settingsPath, JSON.stringify(mergedSettings, null, 2) + '\n');
390
- ok('Hooks wired');
391
- ok('Preserved your existing API keys');
392
- ok('Settings configured');
393
-
394
- // Step 5: Environment
395
- step(5, TOTAL, 'Setting up environment ...');
396
- const envPath = path.join(CLAUDE_DIR, '.env.claude');
397
- if (fs.existsSync(envPath)) {
398
- ok('.env.claude already exists (preserved)');
399
- } else {
400
- fs.copyFileSync(path.join(TEMPLATES_DIR, 'env.claude.template'), envPath);
401
- fs.chmodSync(envPath, 0o600);
402
- warn(`Fill in your API keys: ${c.dim}~/.claude/.env.claude${c.reset}`);
403
- }
455
+ case "remove": {
456
+ const code = (process.argv[4] || "").toUpperCase();
457
+ if (!code) {
458
+ console.log(` ${RED}Usage:${RESET} qualia-framework team remove QS-NAME-NN`);
459
+ process.exit(1);
460
+ }
461
+ const team = readTeamFile();
462
+ if (!team || !team[code]) {
463
+ console.log(` ${YELLOW}!${RESET} ${code} not found in team file.`);
464
+ process.exit(1);
465
+ }
466
+ delete team[code];
467
+ writeTeamFile(team);
468
+ console.log(` ${RED}-${RESET} ${WHITE}${code}${RESET} removed`);
469
+ break;
470
+ }
404
471
 
405
- // Create required directories
406
- for (const d of ['projects', 'session-env', 'backups']) {
407
- fs.mkdirSync(path.join(CLAUDE_DIR, d), { recursive: true });
472
+ default:
473
+ console.log(` ${RED}Usage:${RESET} qualia-framework team <list|add|remove>`);
474
+ process.exit(1);
408
475
  }
476
+ }
409
477
 
410
- // Step 6: Verify
411
- step(6, TOTAL, 'Verifying installation ...');
478
+ // ─── Traces ─────────────────────────────────────────────
412
479
 
413
- let hooksOk = 0;
414
- const hooksTotal = fs.readdirSync(hooksDir).filter(f => f.endsWith('.sh')).length;
415
- for (const f of fs.readdirSync(hooksDir)) {
416
- if (!f.endsWith('.sh')) continue;
480
+ function cmdTraces() {
481
+ banner();
482
+ console.log("");
483
+ const tracesDir = path.join(CLAUDE_DIR, ".qualia-traces");
484
+ if (!fs.existsSync(tracesDir)) {
485
+ console.log(` ${DIM}No traces found. Traces are written by hooks during normal operation.${RESET}`);
486
+ console.log("");
487
+ return;
488
+ }
489
+ const files = fs.readdirSync(tracesDir).filter((f) => f.endsWith(".jsonl")).sort().reverse();
490
+ if (files.length === 0) {
491
+ console.log(` ${DIM}No trace files found.${RESET}`);
492
+ console.log("");
493
+ return;
494
+ }
495
+ const latest = path.join(tracesDir, files[0]);
496
+ const lines = fs.readFileSync(latest, "utf8").trim().split("\n").slice(-20);
497
+ console.log(` ${WHITE}Recent traces${RESET} ${DIM}(${files[0]})${RESET}`);
498
+ console.log("");
499
+ for (const line of lines) {
417
500
  try {
418
- const stat = fs.statSync(path.join(hooksDir, f));
419
- if (stat.mode & 0o111) hooksOk++;
501
+ const e = JSON.parse(line);
502
+ const color = e.result === "block" ? RED : e.result === "allow" ? GREEN : DIM;
503
+ const time = (e.timestamp || "").split("T")[1] || "";
504
+ const ts = time.split(".")[0] || "";
505
+ console.log(` ${DIM}${ts}${RESET} ${color}${e.result}${RESET} ${WHITE}${e.hook}${RESET} ${DIM}${e.duration_ms || 0}ms${RESET}`);
420
506
  } catch {}
421
507
  }
422
- if (hooksOk === hooksTotal) {
423
- ok(`${hooksOk}/${hooksTotal} hooks executable`);
424
- } else {
425
- warn(`${hooksOk}/${hooksTotal} hooks executable`);
426
- }
508
+ console.log("");
509
+ }
427
510
 
428
- try {
429
- JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
430
- ok('settings.json valid JSON');
431
- } catch {
432
- fail('settings.json invalid JSON!');
511
+ // ─── Migrate ────────────────────────────────────────────
512
+
513
+ function cmdMigrate() {
514
+ banner();
515
+ console.log("");
516
+
517
+ const settingsPath = path.join(CLAUDE_DIR, "settings.json");
518
+ if (!fs.existsSync(settingsPath)) {
519
+ console.log(` ${RED}✗${RESET} No settings.json found. Run ${TEAL}qualia-framework install${RESET} first.`);
520
+ console.log("");
521
+ process.exit(1);
433
522
  }
434
523
 
435
- if (fs.existsSync(path.join(CLAUDE_DIR, 'CLAUDE.md'))) {
436
- ok('CLAUDE.md generated');
437
- } else {
438
- fail('CLAUDE.md missing!');
524
+ let settings;
525
+ try {
526
+ settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
527
+ } catch (e) {
528
+ console.log(` ${RED}✗${RESET} Failed to parse settings.json: ${e.message}`);
529
+ process.exit(1);
439
530
  }
440
531
 
441
- // Done
442
- log('');
443
- log(`${c.green}\u2554${'═'.repeat(54)}\u2557${c.reset}`);
444
- log(`${c.green}\u2551${c.reset}${c.bold} \u2713 INSTALLED \u2014 Run /qualia-start in Claude Code ${c.reset}${c.green}\u2551${c.reset}`);
445
- log(`${c.green}\u255A${'═'.repeat(54)}\u255D${c.reset}`);
446
- log('');
447
- log(` ${c.dim}Update later: npx github:Qualiasolutions/qualia-framework update${c.reset}`);
448
- log('');
449
- }
532
+ const cfg = readConfig();
533
+ const fromVersion = cfg.version || "unknown";
534
+ let changes = 0;
450
535
 
451
- async function runUpdate() {
452
- log('');
453
- log(`${c.cyan}\u25C6 Qualia Framework \u2014 Update${c.reset}`);
454
- log('');
536
+ console.log(` ${DIM}Current version:${RESET} ${WHITE}${fromVersion}${RESET}`);
537
+ console.log(` ${DIM}Target version:${RESET} ${WHITE}${PKG.version}${RESET}`);
538
+ console.log("");
455
539
 
456
- if (!fs.existsSync(CLAUDE_DIR)) {
457
- fail('No ~/.claude/ found. Run the installer first (without "update").');
458
- process.exit(1);
459
- }
540
+ // 1. Ensure all 8 hooks are wired (v2 missed block-env-edit and branch-guard)
541
+ const hd = path.join(CLAUDE_DIR, "hooks");
542
+ const nodeCmd = (hookFile) => `node "${path.join(hd, hookFile)}"`;
460
543
 
461
- // Authenticate always require code on update
462
- const profiles = loadProfiles();
463
- const code = await prompt(` Enter your employee code: `);
464
- const profile = profiles[code.toUpperCase()];
544
+ if (!settings.hooks) settings.hooks = {};
465
545
 
466
- if (!profile) {
467
- log('');
468
- fail(`Unknown code "${code}". Contact Fawzi for your employee code.`);
469
- process.exit(1);
546
+ // Check SessionStart hooks
547
+ if (!settings.hooks.SessionStart || !Array.isArray(settings.hooks.SessionStart)) {
548
+ settings.hooks.SessionStart = [{ matcher: ".*", hooks: [{ type: "command", command: nodeCmd("session-start.js"), timeout: 5 }] }];
549
+ changes++;
550
+ console.log(` ${GREEN}+${RESET} Added SessionStart hook`);
470
551
  }
471
552
 
472
- log(` ${c.green}\u2192${c.reset} Updating for ${c.bold}${profile.name}${c.reset} (${profile.role})`);
473
- log('');
553
+ // Check PreToolUse hooks — ensure all critical hooks are present
554
+ 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"];
474
556
 
475
- // Count what's changing
476
- const frameworkDirs = ['skills', 'hooks', 'agents', 'rules', 'qualia-framework', 'scripts', 'knowledge'];
477
- const counts = {};
478
- let totalFiles = 0;
557
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
479
558
 
480
- for (const dir of frameworkDirs) {
481
- const src = path.join(FRAMEWORK_DIR, dir);
482
- if (fs.existsSync(src)) {
483
- const count = countFilesRecursive(src);
484
- counts[dir] = count;
485
- totalFiles += count;
559
+ // Find or create Bash matcher entry
560
+ let bashEntry = settings.hooks.PreToolUse.find(e => e.matcher === "Bash");
561
+ if (!bashEntry) {
562
+ bashEntry = { matcher: "Bash", hooks: [] };
563
+ settings.hooks.PreToolUse.push(bashEntry);
564
+ }
565
+ if (!bashEntry.hooks) bashEntry.hooks = [];
566
+
567
+ for (const hookFile of requiredBashHooks) {
568
+ const cmd = nodeCmd(hookFile);
569
+ const exists = bashEntry.hooks.some(h => h.command && h.command.includes(hookFile));
570
+ if (!exists) {
571
+ const hookDef = { type: "command", command: cmd, timeout: hookFile === "pre-deploy-gate.js" ? 180 : 5 };
572
+ if (hookFile === "branch-guard.js") hookDef.if = "Bash(git push*)";
573
+ if (hookFile === "pre-push.js") { hookDef.if = "Bash(git push*)"; hookDef.timeout = 15; }
574
+ if (hookFile === "pre-deploy-gate.js") hookDef.if = "Bash(vercel --prod*)";
575
+ bashEntry.hooks.push(hookDef);
576
+ changes++;
577
+ console.log(` ${GREEN}+${RESET} Wired ${hookFile} into PreToolUse/Bash`);
486
578
  }
487
579
  }
488
580
 
489
- // Show what will be updated
490
- log(` ${c.bold}Components to update:${c.reset}`);
491
- for (const [dir, count] of Object.entries(counts)) {
492
- log(` ${c.dim}\u2022${c.reset} ${dir} (${count} files)`);
581
+ // Find or create Edit|Write matcher entry
582
+ let editEntry = settings.hooks.PreToolUse.find(e => e.matcher === "Edit|Write");
583
+ if (!editEntry) {
584
+ editEntry = { matcher: "Edit|Write", hooks: [] };
585
+ settings.hooks.PreToolUse.push(editEntry);
493
586
  }
494
- log(` ${c.bold}Total: ${totalFiles} files${c.reset}`);
495
- log('');
587
+ if (!editEntry.hooks) editEntry.hooks = [];
496
588
 
497
- // Update framework files
498
- let updated = 0;
499
- for (const dir of frameworkDirs) {
500
- const src = path.join(FRAMEWORK_DIR, dir);
501
- const dest = path.join(CLAUDE_DIR, dir);
502
- if (fs.existsSync(src)) {
503
- copyDirSync(src, dest);
504
- updated++;
505
- ok(`${dir} (${counts[dir]} files)`);
589
+ for (const hookFile of requiredEditHooks) {
590
+ const cmd = nodeCmd(hookFile);
591
+ const exists = editEntry.hooks.some(h => h.command && h.command.includes(hookFile));
592
+ if (!exists) {
593
+ const hookDef = { type: "command", command: cmd, timeout: hookFile === "migration-guard.js" ? 10 : 5 };
594
+ if (hookFile === "migration-guard.js") hookDef.if = "Edit(*migration*)|Write(*migration*)|Edit(*.sql)|Write(*.sql)";
595
+ editEntry.hooks.push(hookDef);
596
+ changes++;
597
+ console.log(` ${GREEN}+${RESET} Wired ${hookFile} into PreToolUse/Edit|Write`);
506
598
  }
507
599
  }
508
600
 
509
- // Copy standalone files (both .sh and .js for cross-platform)
510
- for (const f of ['statusline-command.sh', 'statusline-command.js', 'askpass.sh']) {
511
- const src = path.join(FRAMEWORK_DIR, f);
512
- if (fs.existsSync(src)) {
513
- fs.copyFileSync(src, path.join(CLAUDE_DIR, f));
514
- try { fs.chmodSync(path.join(CLAUDE_DIR, f), 0o755); } catch {}
515
- }
601
+ // Check PreCompact hook
602
+ if (!settings.hooks.PreCompact) {
603
+ settings.hooks.PreCompact = [{ matcher: "compact", hooks: [{ type: "command", command: nodeCmd("pre-compact.js"), timeout: 15 }] }];
604
+ changes++;
605
+ console.log(` ${GREEN}+${RESET} Added PreCompact hook`);
516
606
  }
517
607
 
518
- // Re-chmod hooks
519
- const hooksDir = path.join(CLAUDE_DIR, 'hooks');
520
- if (fs.existsSync(hooksDir)) {
521
- for (const f of fs.readdirSync(hooksDir)) {
522
- if (f.endsWith('.sh')) try { fs.chmodSync(path.join(hooksDir, f), 0o755); } catch {}
608
+ // 2. Ensure env vars are up to date
609
+ if (!settings.env) settings.env = {};
610
+ const requiredEnv = {
611
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
612
+ CLAUDE_CODE_DISABLE_AUTO_MEMORY: "0",
613
+ MAX_MCP_OUTPUT_TOKENS: "25000",
614
+ CLAUDE_CODE_NO_FLICKER: "1",
615
+ };
616
+ for (const [k, v] of Object.entries(requiredEnv)) {
617
+ if (settings.env[k] !== v) {
618
+ settings.env[k] = v;
619
+ changes++;
620
+ console.log(` ${GREEN}+${RESET} Set env.${k}`);
523
621
  }
524
622
  }
525
623
 
526
- // Re-chmod engine bin
527
- const engineBin = path.join(CLAUDE_DIR, 'qualia-framework', 'bin');
528
- if (fs.existsSync(engineBin)) {
529
- for (const f of fs.readdirSync(engineBin)) {
530
- try { fs.chmodSync(path.join(engineBin, f), 0o755); } catch {}
531
- }
624
+ // 3. Update status line if missing
625
+ if (!settings.statusLine) {
626
+ settings.statusLine = { type: "command", command: `node "${path.join(CLAUDE_DIR, "bin", "statusline.js")}"` };
627
+ changes++;
628
+ console.log(` ${GREEN}+${RESET} Added status line`);
629
+ }
630
+
631
+ // 3b. Add next-devtools MCP if not present
632
+ if (!settings.mcpServers) settings.mcpServers = {};
633
+ if (!settings.mcpServers["next-devtools"]) {
634
+ settings.mcpServers["next-devtools"] = {
635
+ command: "npx",
636
+ args: ["next-devtools-mcp@0.3.10"],
637
+ disabled: false,
638
+ };
639
+ changes++;
640
+ console.log(` ${GREEN}+${RESET} Added next-devtools MCP server`);
532
641
  }
533
642
 
534
- // Regenerate CLAUDE.md for this person's role
535
- const claudeMd = renderCLAUDEmd(profile);
536
- fs.writeFileSync(path.join(CLAUDE_DIR, 'CLAUDE.md'), claudeMd);
537
- ok(`CLAUDE.md regenerated for ${profile.name} (${profile.role})`);
643
+ // 4. Update config version
644
+ cfg.version = PKG.version;
645
+ cfg.migrated_at = new Date().toISOString().split("T")[0];
646
+ writeConfig(cfg);
647
+
648
+ // Write settings
649
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
538
650
 
539
- // Merge settings.json hooks (add new hooks without overwriting user config)
540
- const settingsPath = path.join(CLAUDE_DIR, 'settings.json');
541
- const templateSettingsPath = path.join(TEMPLATES_DIR, 'settings.json');
542
- if (fs.existsSync(settingsPath) && fs.existsSync(templateSettingsPath)) {
543
- const mergedSettings = mergeSettings(settingsPath, templateSettingsPath);
544
- mergedSettings.env.SUDO_ASKPASS = path.join(CLAUDE_DIR, 'askpass.sh');
545
- mergedSettings.env.CLAUDE_ENV_FILE = path.join(CLAUDE_DIR, '.env.claude');
546
- fs.writeFileSync(settingsPath, JSON.stringify(mergedSettings, null, 2) + '\n');
547
- ok('settings.json hooks merged');
651
+ console.log("");
652
+ if (changes === 0) {
653
+ console.log(` ${GREEN}✓${RESET} Already up to date — no migration needed.`);
654
+ } else {
655
+ console.log(` ${GREEN}✓${RESET} Applied ${WHITE}${changes}${RESET} changes. Restart Claude Code to take effect.`);
548
656
  }
657
+ console.log("");
658
+ }
549
659
 
550
- // Clean up removed directories from old versions
551
- for (const stale of ['commands', 'config', 'core', 'qualia-engine']) {
552
- const p = path.join(CLAUDE_DIR, stale);
553
- if (fs.existsSync(p)) {
554
- fs.rmSync(p, { recursive: true, force: true });
555
- ok(`removed stale ${stale}/`);
556
- }
660
+ // ─── Analytics ──────────────────────────────────────────
661
+
662
+ function cmdAnalytics() {
663
+ banner();
664
+ console.log("");
665
+
666
+ const tracesDir = path.join(CLAUDE_DIR, ".qualia-traces");
667
+ if (!fs.existsSync(tracesDir)) {
668
+ console.log(` ${DIM}No traces found. Analytics require hook telemetry data.${RESET}`);
669
+ console.log(` ${DIM}Traces are collected automatically during normal framework use.${RESET}`);
670
+ console.log("");
671
+ return;
557
672
  }
558
673
 
559
- log('');
560
- log(`${c.green}\u25C6${c.reset} ${c.bold}Updated ${updated} components (${totalFiles} files)${c.reset}`);
561
- log(`${c.dim} .env.claude preserved. CLAUDE.md regenerated for ${profile.name}.${c.reset}`);
562
- log('');
563
- }
674
+ const files = fs.readdirSync(tracesDir).filter(f => f.endsWith(".jsonl")).sort();
675
+ if (files.length === 0) {
676
+ console.log(` ${DIM}No trace data yet.${RESET}`);
677
+ console.log("");
678
+ return;
679
+ }
564
680
 
565
- function countFilesRecursive(dir) {
566
- let count = 0;
567
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
568
- if (entry.isDirectory()) {
569
- count += countFilesRecursive(path.join(dir, entry.name));
570
- } else {
571
- count++;
681
+ // Parse all traces
682
+ const traces = [];
683
+ for (const file of files) {
684
+ const lines = fs.readFileSync(path.join(tracesDir, file), "utf8").trim().split("\n");
685
+ for (const line of lines) {
686
+ try { traces.push(JSON.parse(line)); } catch {}
572
687
  }
573
688
  }
574
- return count;
689
+
690
+ // Aggregate stats
691
+ const hookStats = {};
692
+ let totalBlocks = 0;
693
+ let totalAllows = 0;
694
+ let totalDuration = 0;
695
+
696
+ for (const t of traces) {
697
+ const hook = t.hook || "unknown";
698
+ if (!hookStats[hook]) hookStats[hook] = { allow: 0, block: 0, total_ms: 0 };
699
+ if (t.result === "block") { hookStats[hook].block++; totalBlocks++; }
700
+ else { hookStats[hook].allow++; totalAllows++; }
701
+ hookStats[hook].total_ms += t.duration_ms || 0;
702
+ totalDuration += t.duration_ms || 0;
703
+ }
704
+
705
+ // Verification outcomes (from traces that include verification data)
706
+ const verifications = traces.filter(t => t.hook === "state-transition" && t.extra && t.extra.verification);
707
+ const passes = verifications.filter(t => t.extra.verification === "pass").length;
708
+ const fails = verifications.filter(t => t.extra.verification === "fail").length;
709
+
710
+ // Gap cycle data
711
+ const gapTraces = traces.filter(t => t.hook === "state-transition" && t.extra && t.extra.gap_closure);
712
+ const totalGapCycles = gapTraces.length;
713
+
714
+ // Display
715
+ console.log(` ${WHITE}Framework Analytics${RESET}`);
716
+ console.log(` ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
717
+ console.log("");
718
+ console.log(` ${WHITE}Overview${RESET}`);
719
+ console.log(` ${DIM}Trace files:${RESET} ${WHITE}${files.length}${RESET} ${DIM}(${files[0]} → ${files[files.length - 1]})${RESET}`);
720
+ console.log(` ${DIM}Total events:${RESET} ${WHITE}${traces.length}${RESET}`);
721
+ console.log(` ${DIM}Total blocks:${RESET} ${RED}${totalBlocks}${RESET}`);
722
+ console.log(` ${DIM}Total allows:${RESET} ${GREEN}${totalAllows}${RESET}`);
723
+ console.log(` ${DIM}Avg hook time:${RESET} ${WHITE}${traces.length ? Math.round(totalDuration / traces.length) : 0}ms${RESET}`);
724
+ console.log("");
725
+
726
+ // Verification stats
727
+ if (passes + fails > 0) {
728
+ const rate = Math.round((passes / (passes + fails)) * 100);
729
+ console.log(` ${WHITE}Verification Outcomes${RESET}`);
730
+ console.log(` ${DIM}First-pass rate:${RESET} ${rate >= 70 ? GREEN : rate >= 50 ? YELLOW : RED}${rate}%${RESET} ${DIM}(${passes} pass / ${fails} fail)${RESET}`);
731
+ console.log(` ${DIM}Gap cycles:${RESET} ${WHITE}${totalGapCycles}${RESET}`);
732
+ console.log("");
733
+ }
734
+
735
+ // Per-hook breakdown
736
+ console.log(` ${WHITE}Per-Hook Breakdown${RESET}`);
737
+ const sorted = Object.entries(hookStats).sort((a, b) => (b[1].allow + b[1].block) - (a[1].allow + a[1].block));
738
+ for (const [hook, stats] of sorted) {
739
+ const total = stats.allow + stats.block;
740
+ const avg = Math.round(stats.total_ms / total);
741
+ const blockRate = stats.block > 0 ? ` ${RED}${stats.block} blocked${RESET}` : "";
742
+ console.log(` ${DIM}${hook}:${RESET} ${WHITE}${total}${RESET} calls, ${DIM}avg ${avg}ms${RESET}${blockRate}`);
743
+ }
744
+ console.log("");
575
745
  }
576
746
 
577
- function runVerify() {
578
- log('');
579
- log(`${c.cyan}Qualia Framework \u2014 Verify${c.reset}`);
580
- log('');
581
-
582
- let pass = 0;
583
- let total = 0;
584
-
585
- const checks = [
586
- ['CLAUDE.md', () => fs.existsSync(path.join(CLAUDE_DIR, 'CLAUDE.md'))],
587
- ['settings.json', () => { try { JSON.parse(fs.readFileSync(path.join(CLAUDE_DIR, 'settings.json'), 'utf8')); return true; } catch { return false; } }],
588
- ['hooks/', () => fs.existsSync(path.join(CLAUDE_DIR, 'hooks'))],
589
- ['skills/', () => countDir(path.join(CLAUDE_DIR, 'skills'), 'dirs') > 50],
590
- ['agents/', () => countDir(path.join(CLAUDE_DIR, 'agents'), 'files') > 15],
591
- ['qualia-framework/', () => fs.existsSync(path.join(CLAUDE_DIR, 'qualia-framework', 'bin'))],
592
- ['rules/', () => countDir(path.join(CLAUDE_DIR, 'rules'), 'files') >= 4],
593
- ['.env.claude', () => fs.existsSync(path.join(CLAUDE_DIR, '.env.claude'))],
594
- ];
595
-
596
- for (const [name, check] of checks) {
597
- total++;
598
- if (check()) { ok(name); pass++; }
599
- else { fail(name); }
600
- }
601
-
602
- log('');
603
- if (pass === total) {
604
- log(`${c.green}All ${total} checks passed.${c.reset}`);
605
- } else {
606
- log(`${c.yellow}${pass}/${total} checks passed.${c.reset}`);
607
- }
608
- log('');
747
+ function cmdHelp() {
748
+ banner();
749
+ console.log("");
750
+ console.log(` ${WHITE}Commands:${RESET}`);
751
+ console.log(` qualia-framework ${TEAL}install${RESET} Install or reinstall the framework`);
752
+ console.log(` qualia-framework ${TEAL}update${RESET} Update to the latest version`);
753
+ console.log(` qualia-framework ${TEAL}version${RESET} Show installed version + check for updates`);
754
+ console.log(` qualia-framework ${TEAL}uninstall${RESET} Clean removal from ~/.claude/ (${DIM}-y to skip prompts${RESET})`);
755
+ console.log(` qualia-framework ${TEAL}migrate${RESET} Migrate settings from v2 to v3`);
756
+ console.log(` qualia-framework ${TEAL}team${RESET} Manage team members (${DIM}list|add|remove${RESET})`);
757
+ console.log(` qualia-framework ${TEAL}traces${RESET} View recent hook telemetry`);
758
+ console.log(` qualia-framework ${TEAL}analytics${RESET} Show outcome scoring & gap cycle stats`);
759
+ console.log("");
760
+ console.log(` ${WHITE}After install:${RESET}`);
761
+ console.log(` ${TG}/qualia${RESET} What should I do next?`);
762
+ console.log(` ${TG}/qualia-new${RESET} Set up a new project`);
763
+ console.log(` ${TG}/qualia-plan${RESET} Plan a phase`);
764
+ console.log(` ${TG}/qualia-build${RESET} Build it (parallel tasks)`);
765
+ console.log(` ${TG}/qualia-verify${RESET} Verify it works`);
766
+ console.log(` ${TG}/qualia-design${RESET} One-shot design fix`);
767
+ console.log(` ${TG}/qualia-debug${RESET} Structured debugging`);
768
+ console.log(` ${TG}/qualia-review${RESET} Production audit`);
769
+ console.log(` ${TG}/qualia-ship${RESET} Deploy to production`);
770
+ console.log(` ${TG}/qualia-report${RESET} Log your work`);
771
+ console.log("");
609
772
  }
610
773
 
611
- main().catch(err => {
612
- console.error(`${c.red}Error:${c.reset}`, err.message);
613
- process.exit(1);
614
- });
774
+
775
+ // ─── Main ────────────────────────────────────────────────
776
+ const cmd = process.argv[2];
777
+
778
+ switch (cmd) {
779
+ case "install":
780
+ cmdInstall();
781
+ break;
782
+ case "version":
783
+ case "-v":
784
+ case "--version":
785
+ cmdVersion();
786
+ break;
787
+ case "update":
788
+ case "upgrade":
789
+ cmdUpdate();
790
+ break;
791
+ case "uninstall":
792
+ case "remove":
793
+ cmdUninstall().catch((e) => {
794
+ console.error(`${RED} ✗ Uninstall failed: ${e.message}${RESET}`);
795
+ process.exit(1);
796
+ });
797
+ break;
798
+ case "team":
799
+ cmdTeam();
800
+ break;
801
+ case "traces":
802
+ cmdTraces();
803
+ break;
804
+ case "migrate":
805
+ cmdMigrate();
806
+ break;
807
+ case "analytics":
808
+ case "stats":
809
+ cmdAnalytics();
810
+ break;
811
+ default:
812
+ cmdHelp();
813
+ }