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.
- package/CLAUDE.md +3 -4
- package/README.md +59 -23
- package/agents/plan-checker.md +158 -0
- package/agents/planner.md +52 -0
- package/agents/research-synthesizer.md +86 -0
- package/agents/researcher.md +119 -0
- package/agents/roadmapper.md +157 -0
- package/agents/verifier.md +180 -32
- package/bin/cli.js +403 -9
- package/bin/install.js +219 -70
- package/bin/qualia-ui.js +11 -11
- package/bin/state.js +200 -6
- package/bin/statusline.js +4 -4
- package/docs/erp-contract.md +161 -0
- package/hooks/branch-guard.js +23 -2
- package/hooks/migration-guard.js +23 -0
- package/hooks/pre-compact.js +20 -0
- package/hooks/pre-deploy-gate.js +39 -0
- package/hooks/pre-push.js +20 -0
- package/hooks/session-start.js +16 -43
- package/package.json +6 -4
- package/references/questioning.md +123 -0
- package/rules/infrastructure.md +87 -0
- package/skills/qualia/SKILL.md +1 -0
- package/skills/qualia-build/SKILL.md +18 -0
- package/skills/qualia-design/SKILL.md +14 -8
- package/skills/qualia-discuss/SKILL.md +115 -0
- package/skills/qualia-help/SKILL.md +60 -0
- package/skills/qualia-learn/SKILL.md +27 -4
- package/skills/qualia-map/SKILL.md +145 -0
- package/skills/qualia-milestone/SKILL.md +148 -0
- package/skills/qualia-new/SKILL.md +374 -229
- package/skills/qualia-plan/SKILL.md +135 -30
- package/skills/qualia-polish/SKILL.md +167 -117
- package/skills/qualia-report/SKILL.md +17 -8
- package/skills/qualia-research/SKILL.md +124 -0
- package/skills/qualia-review/SKILL.md +126 -41
- package/skills/qualia-test/SKILL.md +134 -0
- package/skills/qualia-verify/SKILL.md +1 -1
- package/templates/DESIGN.md +440 -102
- package/templates/help.html +476 -0
- package/templates/phase-context.md +48 -0
- package/templates/plan.md +14 -0
- package/templates/projects/ai-agent.md +55 -0
- package/templates/projects/mobile-app.md +56 -0
- package/templates/projects/voice-agent.md +55 -0
- package/templates/projects/website.md +58 -0
- package/templates/requirements.md +69 -0
- package/templates/research-project/ARCHITECTURE.md +70 -0
- package/templates/research-project/FEATURES.md +60 -0
- package/templates/research-project/PITFALLS.md +73 -0
- package/templates/research-project/STACK.md +51 -0
- package/templates/research-project/SUMMARY.md +86 -0
- package/templates/roadmap.md +71 -0
- package/tests/bin.test.sh +20 -6
- package/tests/hooks.test.sh +76 -7
- package/tests/runner.js +1915 -0
- 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
|
-
//
|
|
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
|
-
//
|
|
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(`
|
|
376
|
-
console.log(`
|
|
377
|
-
console.log(`
|
|
378
|
-
console.log(`
|
|
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
|
}
|