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/CLAUDE.md +4 -3
- package/README.md +5 -10
- package/agents/planner.md +0 -52
- package/agents/verifier.md +32 -180
- package/bin/cli.js +9 -403
- package/bin/install.js +61 -113
- package/bin/qualia-ui.js +15 -0
- package/bin/state.js +6 -200
- package/bin/statusline.js +4 -4
- package/hooks/auto-update.js +30 -8
- package/hooks/branch-guard.js +2 -23
- package/hooks/migration-guard.js +0 -23
- package/hooks/pre-compact.js +0 -20
- package/hooks/pre-deploy-gate.js +0 -39
- package/hooks/pre-push.js +0 -20
- package/hooks/session-start.js +44 -0
- package/package.json +4 -5
- package/skills/qualia/SKILL.md +0 -1
- package/skills/qualia-build/SKILL.md +0 -18
- package/skills/qualia-design/SKILL.md +8 -14
- package/skills/qualia-learn/SKILL.md +4 -27
- package/skills/qualia-optimize/SKILL.md +417 -0
- package/skills/qualia-polish/SKILL.md +117 -167
- package/skills/qualia-report/SKILL.md +8 -17
- package/skills/qualia-review/SKILL.md +41 -126
- package/skills/qualia-verify/SKILL.md +1 -1
- package/templates/DESIGN.md +102 -440
- package/templates/plan.md +0 -14
- package/tests/bin.test.sh +6 -20
- package/tests/hooks.test.sh +7 -76
- package/tests/state.test.sh +11 -189
- package/docs/erp-contract.md +0 -161
- package/hooks/block-env-edit.js +0 -52
- package/rules/infrastructure.md +0 -87
- package/skills/qualia-help/SKILL.md +0 -60
- package/skills/qualia-test/SKILL.md +0 -134
- package/templates/help.html +0 -476
- package/tests/runner.js +0 -1956
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
|
-
//
|
|
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
|
-
//
|
|
149
|
-
const QUALIA_RULE_FILES = ["security.md", "frontend.md", "design-reference.md", "deployment.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}
|
|
752
|
-
console.log(` qualia-framework ${TEAL}update${RESET}
|
|
753
|
-
console.log(` qualia-framework ${TEAL}version${RESET}
|
|
754
|
-
console.log(` qualia-framework ${TEAL}uninstall${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
|
}
|