qualia-framework 3.2.0 → 3.3.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 (58) hide show
  1. package/CLAUDE.md +3 -4
  2. package/README.md +59 -23
  3. package/agents/plan-checker.md +158 -0
  4. package/agents/planner.md +52 -0
  5. package/agents/research-synthesizer.md +86 -0
  6. package/agents/researcher.md +119 -0
  7. package/agents/roadmapper.md +157 -0
  8. package/agents/verifier.md +180 -32
  9. package/bin/cli.js +403 -9
  10. package/bin/install.js +219 -70
  11. package/bin/qualia-ui.js +11 -11
  12. package/bin/state.js +200 -6
  13. package/bin/statusline.js +4 -4
  14. package/docs/erp-contract.md +161 -0
  15. package/hooks/branch-guard.js +23 -2
  16. package/hooks/migration-guard.js +23 -0
  17. package/hooks/pre-compact.js +20 -0
  18. package/hooks/pre-deploy-gate.js +39 -0
  19. package/hooks/pre-push.js +20 -0
  20. package/hooks/session-start.js +16 -43
  21. package/package.json +6 -4
  22. package/references/questioning.md +123 -0
  23. package/rules/infrastructure.md +87 -0
  24. package/skills/qualia/SKILL.md +1 -0
  25. package/skills/qualia-build/SKILL.md +18 -0
  26. package/skills/qualia-design/SKILL.md +14 -8
  27. package/skills/qualia-discuss/SKILL.md +115 -0
  28. package/skills/qualia-help/SKILL.md +60 -0
  29. package/skills/qualia-learn/SKILL.md +27 -4
  30. package/skills/qualia-map/SKILL.md +145 -0
  31. package/skills/qualia-milestone/SKILL.md +148 -0
  32. package/skills/qualia-new/SKILL.md +374 -229
  33. package/skills/qualia-plan/SKILL.md +135 -30
  34. package/skills/qualia-polish/SKILL.md +167 -117
  35. package/skills/qualia-report/SKILL.md +17 -8
  36. package/skills/qualia-research/SKILL.md +124 -0
  37. package/skills/qualia-review/SKILL.md +126 -41
  38. package/skills/qualia-test/SKILL.md +134 -0
  39. package/skills/qualia-verify/SKILL.md +1 -1
  40. package/templates/DESIGN.md +440 -102
  41. package/templates/help.html +476 -0
  42. package/templates/phase-context.md +48 -0
  43. package/templates/plan.md +14 -0
  44. package/templates/projects/ai-agent.md +55 -0
  45. package/templates/projects/mobile-app.md +56 -0
  46. package/templates/projects/voice-agent.md +55 -0
  47. package/templates/projects/website.md +58 -0
  48. package/templates/requirements.md +69 -0
  49. package/templates/research-project/ARCHITECTURE.md +70 -0
  50. package/templates/research-project/FEATURES.md +60 -0
  51. package/templates/research-project/PITFALLS.md +73 -0
  52. package/templates/research-project/STACK.md +51 -0
  53. package/templates/research-project/SUMMARY.md +86 -0
  54. package/templates/roadmap.md +71 -0
  55. package/tests/bin.test.sh +20 -6
  56. package/tests/hooks.test.sh +76 -7
  57. package/tests/runner.js +1915 -0
  58. package/tests/state.test.sh +189 -11
package/bin/cli.js CHANGED
@@ -126,15 +126,14 @@ 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
- // 7 Qualia hook filenames — only these are removed from ~/.claude/hooks/,
129
+ // 8 Qualia hook filenames — only these are removed from ~/.claude/hooks/,
130
130
  // any other hooks the user dropped in there are left alone.
131
- // Note: block-env-edit.js was retired in v3.2.0; the installer removes any
132
- // lingering copy, and this uninstall list no longer references it.
133
131
  const QUALIA_HOOK_FILES = [
134
132
  "session-start.js",
135
133
  "auto-update.js",
136
134
  "branch-guard.js",
137
135
  "pre-push.js",
136
+ "block-env-edit.js",
138
137
  "migration-guard.js",
139
138
  "pre-deploy-gate.js",
140
139
  "pre-compact.js",
@@ -146,8 +145,8 @@ const QUALIA_AGENT_FILES = ["planner.md", "builder.md", "verifier.md", "qa-brows
146
145
  // 3 Qualia bin scripts.
147
146
  const QUALIA_BIN_FILES = ["state.js", "qualia-ui.js", "statusline.js"];
148
147
 
149
- // 4 Qualia rules.
150
- const QUALIA_RULE_FILES = ["security.md", "frontend.md", "design-reference.md", "deployment.md"];
148
+ // 5 Qualia rules.
149
+ const QUALIA_RULE_FILES = ["security.md", "frontend.md", "design-reference.md", "deployment.md", "infrastructure.md"];
151
150
 
152
151
  function promptYesNo(question, defaultYes) {
153
152
  return new Promise((resolve) => {
@@ -333,8 +332,12 @@ async function cmdUninstall() {
333
332
  safeUnlink(path.join(CLAUDE_DIR, ".qualia-config.json"), counters);
334
333
  safeUnlink(path.join(CLAUDE_DIR, ".qualia-last-update-check"), counters);
335
334
  safeUnlink(path.join(CLAUDE_DIR, ".erp-api-key"), counters);
335
+ safeUnlink(path.join(CLAUDE_DIR, ".qualia-team.json"), counters);
336
336
  safeUnlink(path.join(CLAUDE_DIR, "qualia-guide.md"), counters);
337
337
 
338
+ // Traces directory.
339
+ safeRmDir(path.join(CLAUDE_DIR, ".qualia-traces"), counters);
340
+
338
341
  // Clean settings.json surgically.
339
342
  cleanSettingsJson(counters);
340
343
 
@@ -368,14 +371,391 @@ async function cmdUninstall() {
368
371
  console.log("");
369
372
  }
370
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;
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(); }
410
+ }
411
+ return args;
412
+ }
413
+
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}`);
429
+ }
430
+ console.log("");
431
+ break;
432
+ }
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;
453
+ }
454
+
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
+ }
471
+
472
+ default:
473
+ console.log(` ${RED}Usage:${RESET} qualia-framework team <list|add|remove>`);
474
+ process.exit(1);
475
+ }
476
+ }
477
+
478
+ // ─── Traces ─────────────────────────────────────────────
479
+
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) {
500
+ try {
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}`);
506
+ } catch {}
507
+ }
508
+ console.log("");
509
+ }
510
+
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);
522
+ }
523
+
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);
530
+ }
531
+
532
+ const cfg = readConfig();
533
+ const fromVersion = cfg.version || "unknown";
534
+ let changes = 0;
535
+
536
+ console.log(` ${DIM}Current version:${RESET} ${WHITE}${fromVersion}${RESET}`);
537
+ console.log(` ${DIM}Target version:${RESET} ${WHITE}${PKG.version}${RESET}`);
538
+ console.log("");
539
+
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)}"`;
543
+
544
+ if (!settings.hooks) settings.hooks = {};
545
+
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`);
551
+ }
552
+
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"];
556
+
557
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
558
+
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`);
578
+ }
579
+ }
580
+
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);
586
+ }
587
+ if (!editEntry.hooks) editEntry.hooks = [];
588
+
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`);
598
+ }
599
+ }
600
+
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`);
606
+ }
607
+
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}`);
621
+ }
622
+ }
623
+
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`);
641
+ }
642
+
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");
650
+
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.`);
656
+ }
657
+ console.log("");
658
+ }
659
+
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;
672
+ }
673
+
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
+ }
680
+
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 {}
687
+ }
688
+ }
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("");
745
+ }
746
+
371
747
  function cmdHelp() {
372
748
  banner();
373
749
  console.log("");
374
750
  console.log(` ${WHITE}Commands:${RESET}`);
375
- console.log(` npx qualia-framework ${TEAL}install${RESET} Install or reinstall the framework`);
376
- console.log(` npx qualia-framework ${TEAL}update${RESET} Update to the latest version`);
377
- console.log(` npx qualia-framework ${TEAL}version${RESET} Show installed version + check for updates`);
378
- console.log(` npx qualia-framework ${TEAL}uninstall${RESET} Clean removal from ~/.claude/ (${DIM}-y to skip prompts${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`);
379
759
  console.log("");
380
760
  console.log(` ${WHITE}After install:${RESET}`);
381
761
  console.log(` ${TG}/qualia${RESET} What should I do next?`);
@@ -391,6 +771,7 @@ function cmdHelp() {
391
771
  console.log("");
392
772
  }
393
773
 
774
+
394
775
  // ─── Main ────────────────────────────────────────────────
395
776
  const cmd = process.argv[2];
396
777
 
@@ -414,6 +795,19 @@ switch (cmd) {
414
795
  process.exit(1);
415
796
  });
416
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;
417
811
  default:
418
812
  cmdHelp();
419
813
  }