qualia-framework 3.1.0 → 3.2.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.
package/bin/cli.js CHANGED
@@ -126,14 +126,15 @@ function cmdUpdate() {
126
126
  // non-Qualia entries in settings.json (other hooks, user env vars, etc.).
127
127
  // --yes / -y skips the confirmation prompt for scripted use.
128
128
 
129
- // 8 Qualia hook filenames — only these are removed from ~/.claude/hooks/,
129
+ // 7 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.
131
133
  const QUALIA_HOOK_FILES = [
132
134
  "session-start.js",
133
135
  "auto-update.js",
134
136
  "branch-guard.js",
135
137
  "pre-push.js",
136
- "block-env-edit.js",
137
138
  "migration-guard.js",
138
139
  "pre-deploy-gate.js",
139
140
  "pre-compact.js",
@@ -145,8 +146,8 @@ const QUALIA_AGENT_FILES = ["planner.md", "builder.md", "verifier.md", "qa-brows
145
146
  // 3 Qualia bin scripts.
146
147
  const QUALIA_BIN_FILES = ["state.js", "qualia-ui.js", "statusline.js"];
147
148
 
148
- // 5 Qualia rules.
149
- const QUALIA_RULE_FILES = ["security.md", "frontend.md", "design-reference.md", "deployment.md", "infrastructure.md"];
149
+ // 4 Qualia rules.
150
+ const QUALIA_RULE_FILES = ["security.md", "frontend.md", "design-reference.md", "deployment.md"];
150
151
 
151
152
  function promptYesNo(question, defaultYes) {
152
153
  return new Promise((resolve) => {
@@ -332,12 +333,8 @@ async function cmdUninstall() {
332
333
  safeUnlink(path.join(CLAUDE_DIR, ".qualia-config.json"), counters);
333
334
  safeUnlink(path.join(CLAUDE_DIR, ".qualia-last-update-check"), counters);
334
335
  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
-
341
338
  // Clean settings.json surgically.
342
339
  cleanSettingsJson(counters);
343
340
 
@@ -371,391 +368,14 @@ async function cmdUninstall() {
371
368
  console.log("");
372
369
  }
373
370
 
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
-
747
371
  function cmdHelp() {
748
372
  banner();
749
373
  console.log("");
750
374
  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`);
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})`);
759
379
  console.log("");
760
380
  console.log(` ${WHITE}After install:${RESET}`);
761
381
  console.log(` ${TG}/qualia${RESET} What should I do next?`);
@@ -771,7 +391,6 @@ function cmdHelp() {
771
391
  console.log("");
772
392
  }
773
393
 
774
-
775
394
  // ─── Main ────────────────────────────────────────────────
776
395
  const cmd = process.argv[2];
777
396
 
@@ -795,19 +414,6 @@ switch (cmd) {
795
414
  process.exit(1);
796
415
  });
797
416
  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
417
  default:
812
418
  cmdHelp();
813
419
  }