qualia-framework 4.1.1 → 4.4.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 (43) hide show
  1. package/README.md +15 -11
  2. package/agents/builder.md +28 -0
  3. package/agents/research-synthesizer.md +7 -0
  4. package/bin/agent-runs.js +233 -0
  5. package/bin/cli.js +355 -16
  6. package/bin/install.js +87 -6
  7. package/bin/knowledge-flush.js +164 -0
  8. package/bin/knowledge.js +317 -0
  9. package/bin/plan-contract.js +220 -0
  10. package/bin/state.js +15 -9
  11. package/docs/agent-runs.md +273 -0
  12. package/docs/journey-demo.html +1008 -0
  13. package/docs/plan-contract.md +321 -0
  14. package/docs/reviews/v4.1.0-audit.html +1488 -0
  15. package/docs/reviews/v4.1.0-audit.md +263 -0
  16. package/hooks/auto-update.js +3 -7
  17. package/hooks/git-guardrails.js +167 -0
  18. package/hooks/pre-compact.js +22 -11
  19. package/hooks/pre-deploy-gate.js +16 -2
  20. package/hooks/pre-push.js +22 -2
  21. package/hooks/stop-session-log.js +180 -0
  22. package/package.json +8 -2
  23. package/skills/qualia-build/SKILL.md +5 -5
  24. package/skills/qualia-debug/SKILL.md +1 -1
  25. package/skills/qualia-design/SKILL.md +15 -0
  26. package/skills/qualia-flush/SKILL.md +200 -0
  27. package/skills/qualia-learn/SKILL.md +47 -37
  28. package/skills/qualia-new/SKILL.md +1 -1
  29. package/skills/qualia-plan/SKILL.md +3 -2
  30. package/skills/qualia-postmortem/SKILL.md +238 -0
  31. package/skills/qualia-quick/SKILL.md +1 -1
  32. package/skills/qualia-report/SKILL.md +1 -1
  33. package/skills/qualia-review/SKILL.md +3 -2
  34. package/skills/qualia-ship/SKILL.md +12 -10
  35. package/skills/qualia-verify/SKILL.md +60 -0
  36. package/templates/help.html +13 -7
  37. package/templates/knowledge/agents.md +71 -0
  38. package/templates/knowledge/index.md +47 -0
  39. package/tests/bin.test.sh +322 -12
  40. package/tests/hooks.test.sh +131 -20
  41. package/tests/lib.test.sh +217 -0
  42. package/tests/runner.js +103 -77
  43. package/tests/state.test.sh +4 -3
package/bin/cli.js CHANGED
@@ -29,7 +29,8 @@ function readConfig() {
29
29
 
30
30
  function writeConfig(cfg) {
31
31
  if (!fs.existsSync(CLAUDE_DIR)) fs.mkdirSync(CLAUDE_DIR, { recursive: true });
32
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n");
32
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n", { mode: 0o600 });
33
+ try { fs.chmodSync(CONFIG_FILE, 0o600); } catch {}
33
34
  }
34
35
 
35
36
  function banner() {
@@ -139,6 +140,8 @@ const QUALIA_HOOK_FILES = [
139
140
  "migration-guard.js",
140
141
  "pre-deploy-gate.js",
141
142
  "pre-compact.js",
143
+ "git-guardrails.js",
144
+ "stop-session-log.js",
142
145
  ];
143
146
  const QUALIA_LEGACY_HOOK_FILES = [
144
147
  "block-env-edit.js", // removed in v3.2.0
@@ -157,10 +160,10 @@ const QUALIA_AGENT_FILES = [
157
160
  ];
158
161
 
159
162
  // 3 Qualia bin scripts.
160
- const QUALIA_BIN_FILES = ["state.js", "qualia-ui.js", "statusline.js"];
163
+ const QUALIA_BIN_FILES = ["state.js", "qualia-ui.js", "statusline.js", "knowledge.js", "knowledge-flush.js", "plan-contract.js", "agent-runs.js"];
161
164
 
162
- // 5 Qualia rules.
163
- const QUALIA_RULE_FILES = ["security.md", "frontend.md", "design-reference.md", "deployment.md", "infrastructure.md"];
165
+ // 6 Qualia rules.
166
+ const QUALIA_RULE_FILES = ["security.md", "frontend.md", "design-reference.md", "deployment.md", "infrastructure.md", "grounding.md"];
164
167
 
165
168
  function promptYesNo(question, defaultYes) {
166
169
  return new Promise((resolve) => {
@@ -551,7 +554,7 @@ function cmdMigrate() {
551
554
  console.log(` ${DIM}Target version:${RESET} ${WHITE}${PKG.version}${RESET}`);
552
555
  console.log("");
553
556
 
554
- // 1. Ensure all 8 hooks are wired (v2 missed block-env-edit and branch-guard)
557
+ // 1. Ensure the full v4.3 hook set is wired.
555
558
  const hd = path.join(CLAUDE_DIR, "hooks");
556
559
  const nodeCmd = (hookFile) => `node "${path.join(hd, hookFile)}"`;
557
560
 
@@ -562,10 +565,19 @@ function cmdMigrate() {
562
565
  settings.hooks.SessionStart = [{ matcher: ".*", hooks: [{ type: "command", command: nodeCmd("session-start.js"), timeout: 5 }] }];
563
566
  changes++;
564
567
  console.log(` ${GREEN}+${RESET} Added SessionStart hook`);
568
+ } else {
569
+ const hasSessionStart = settings.hooks.SessionStart.some(e =>
570
+ Array.isArray(e.hooks) && e.hooks.some(h => typeof h.command === "string" && h.command.includes("session-start.js"))
571
+ );
572
+ if (!hasSessionStart) {
573
+ settings.hooks.SessionStart.push({ matcher: ".*", hooks: [{ type: "command", command: nodeCmd("session-start.js"), timeout: 5 }] });
574
+ changes++;
575
+ console.log(` ${GREEN}+${RESET} Wired session-start.js into SessionStart`);
576
+ }
565
577
  }
566
578
 
567
579
  // Check PreToolUse hooks — ensure all critical hooks are present
568
- const requiredBashHooks = ["auto-update.js", "branch-guard.js", "pre-push.js", "pre-deploy-gate.js"];
580
+ const requiredBashHooks = ["auto-update.js", "git-guardrails.js", "branch-guard.js", "pre-push.js", "pre-deploy-gate.js"];
569
581
  const requiredEditHooks = ["migration-guard.js"];
570
582
 
571
583
  if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
@@ -593,9 +605,10 @@ function cmdMigrate() {
593
605
  const exists = bashEntry.hooks.some(h => extractScriptName(h.command) === targetName);
594
606
  if (!exists) {
595
607
  const hookDef = { type: "command", command: cmd, timeout: hookFile === "pre-deploy-gate.js" ? 180 : 5 };
596
- if (hookFile === "branch-guard.js") hookDef.if = "Bash(git push*)";
608
+ if (hookFile === "git-guardrails.js") hookDef.statusMessage = "⬢ Checking git safety...";
609
+ if (hookFile === "branch-guard.js") { hookDef.if = "Bash(git push*)"; hookDef.statusMessage = "⬢ Checking branch permissions..."; }
597
610
  if (hookFile === "pre-push.js") { hookDef.if = "Bash(git push*)"; hookDef.timeout = 15; }
598
- if (hookFile === "pre-deploy-gate.js") hookDef.if = "Bash(vercel --prod*)";
611
+ if (hookFile === "pre-deploy-gate.js") { hookDef.if = "Bash(vercel --prod*)"; hookDef.timeout = 180; hookDef.statusMessage = "⬢ Running quality gates..."; }
599
612
  bashEntry.hooks.push(hookDef);
600
613
  changes++;
601
614
  console.log(` ${GREEN}+${RESET} Wired ${hookFile} into PreToolUse/Bash`);
@@ -616,7 +629,7 @@ function cmdMigrate() {
616
629
  const exists = editEntry.hooks.some(h => extractScriptName(h.command) === targetName);
617
630
  if (!exists) {
618
631
  const hookDef = { type: "command", command: cmd, timeout: hookFile === "migration-guard.js" ? 10 : 5 };
619
- if (hookFile === "migration-guard.js") hookDef.if = "Edit(*migration*)|Write(*migration*)|Edit(*.sql)|Write(*.sql)";
632
+ if (hookFile === "migration-guard.js") { hookDef.if = "Edit(*migration*)|Write(*migration*)|Edit(*.sql)|Write(*.sql)"; hookDef.statusMessage = "⬢ Checking migration safety..."; }
620
633
  editEntry.hooks.push(hookDef);
621
634
  changes++;
622
635
  console.log(` ${GREEN}+${RESET} Wired ${hookFile} into PreToolUse/Edit|Write`);
@@ -624,10 +637,42 @@ function cmdMigrate() {
624
637
  }
625
638
 
626
639
  // Check PreCompact hook
627
- if (!settings.hooks.PreCompact) {
640
+ if (!settings.hooks.PreCompact || !Array.isArray(settings.hooks.PreCompact)) {
628
641
  settings.hooks.PreCompact = [{ matcher: "compact", hooks: [{ type: "command", command: nodeCmd("pre-compact.js"), timeout: 15 }] }];
629
642
  changes++;
630
643
  console.log(` ${GREEN}+${RESET} Added PreCompact hook`);
644
+ } else {
645
+ let compactEntry = settings.hooks.PreCompact.find(e => e.matcher === "compact");
646
+ if (!compactEntry) {
647
+ compactEntry = { matcher: "compact", hooks: [] };
648
+ settings.hooks.PreCompact.push(compactEntry);
649
+ }
650
+ if (!compactEntry.hooks) compactEntry.hooks = [];
651
+ const exists = compactEntry.hooks.some(h => extractScriptName(h.command) === "pre-compact.js");
652
+ if (!exists) {
653
+ compactEntry.hooks.push({ type: "command", command: nodeCmd("pre-compact.js"), timeout: 15, statusMessage: "⬢ Saving state..." });
654
+ changes++;
655
+ console.log(` ${GREEN}+${RESET} Wired pre-compact.js into PreCompact`);
656
+ }
657
+ }
658
+
659
+ if (!settings.hooks.Stop || !Array.isArray(settings.hooks.Stop)) {
660
+ settings.hooks.Stop = [{ matcher: ".*", hooks: [{ type: "command", command: nodeCmd("stop-session-log.js"), timeout: 5 }] }];
661
+ changes++;
662
+ console.log(` ${GREEN}+${RESET} Added Stop hook`);
663
+ } else {
664
+ let stopEntry = settings.hooks.Stop.find(e => e.matcher === ".*");
665
+ if (!stopEntry) {
666
+ stopEntry = { matcher: ".*", hooks: [] };
667
+ settings.hooks.Stop.push(stopEntry);
668
+ }
669
+ if (!stopEntry.hooks) stopEntry.hooks = [];
670
+ const exists = stopEntry.hooks.some(h => extractScriptName(h.command) === "stop-session-log.js");
671
+ if (!exists) {
672
+ stopEntry.hooks.push({ type: "command", command: nodeCmd("stop-session-log.js"), timeout: 5 });
673
+ changes++;
674
+ console.log(` ${GREEN}+${RESET} Wired stop-session-log.js into Stop`);
675
+ }
631
676
  }
632
677
 
633
678
  // 2. Ensure env vars are up to date
@@ -728,12 +773,12 @@ function cmdAnalytics() {
728
773
  }
729
774
 
730
775
  // Verification outcomes (from traces that include verification data)
731
- const verifications = traces.filter(t => t.hook === "state-transition" && t.extra && t.extra.verification);
732
- const passes = verifications.filter(t => t.extra.verification === "pass").length;
733
- const fails = verifications.filter(t => t.extra.verification === "fail").length;
776
+ const verifications = traces.filter(t => t.hook === "state-transition" && t.verification);
777
+ const passes = verifications.filter(t => t.verification === "pass").length;
778
+ const fails = verifications.filter(t => t.verification === "fail").length;
734
779
 
735
780
  // Gap cycle data
736
- const gapTraces = traces.filter(t => t.hook === "state-transition" && t.extra && t.extra.gap_closure);
781
+ const gapTraces = traces.filter(t => t.hook === "state-transition" && t.gap_closure);
737
782
  const totalGapCycles = gapTraces.length;
738
783
 
739
784
  // Display
@@ -779,8 +824,9 @@ function cmdErpPing() {
779
824
  console.log("");
780
825
 
781
826
  const cfg = readConfig();
827
+ const args = new Set(process.argv.slice(3));
782
828
  const erpUrl = (cfg.erp && cfg.erp.url) || "https://portal.qualiasolutions.net";
783
- const erpEnabled = !(cfg.erp && cfg.erp.enabled === false);
829
+ let erpEnabled = !(cfg.erp && cfg.erp.enabled === false);
784
830
  const keyFile = path.join(CLAUDE_DIR, ".erp-api-key");
785
831
 
786
832
  console.log(` ${DIM}URL:${RESET} ${WHITE}${erpUrl}${RESET}`);
@@ -800,6 +846,19 @@ function cmdErpPing() {
800
846
  console.log(` ${DIM}Key:${RESET} ${GREEN}present${RESET} ${DIM}(${apiKey.length} bytes)${RESET}`);
801
847
  console.log("");
802
848
 
849
+ if (!erpEnabled && args.has("--enable")) {
850
+ cfg.erp = {
851
+ ...(cfg.erp || {}),
852
+ enabled: true,
853
+ url: erpUrl,
854
+ api_key_file: ".erp-api-key",
855
+ };
856
+ writeConfig(cfg);
857
+ erpEnabled = true;
858
+ console.log(` ${GREEN}✓ ERP enabled in config.${RESET}`);
859
+ console.log("");
860
+ }
861
+
803
862
  if (!erpEnabled) {
804
863
  console.log(` ${YELLOW}ERP is disabled in config. Enable with:${RESET}`);
805
864
  console.log(` ${DIM} qualia-framework erp-ping --enable${RESET}`);
@@ -872,6 +931,266 @@ function cmdErpPing() {
872
931
  process.exit(1);
873
932
  }
874
933
 
934
+ function cmdSetErpKey() {
935
+ banner();
936
+ console.log("");
937
+
938
+ const keyFile = path.join(CLAUDE_DIR, ".erp-api-key");
939
+ const rawArgs = process.argv.slice(3);
940
+ const clear = rawArgs.includes("--clear");
941
+
942
+ if (!fs.existsSync(CLAUDE_DIR)) fs.mkdirSync(CLAUDE_DIR, { recursive: true });
943
+
944
+ if (clear) {
945
+ try { fs.unlinkSync(keyFile); } catch {}
946
+ const cfg = readConfig();
947
+ cfg.erp = { ...(cfg.erp || {}), enabled: false, url: (cfg.erp && cfg.erp.url) || "https://portal.qualiasolutions.net", api_key_file: ".erp-api-key" };
948
+ writeConfig(cfg);
949
+ console.log(` ${GREEN}✓${RESET} ERP key removed and ERP disabled.`);
950
+ console.log("");
951
+ return;
952
+ }
953
+
954
+ let key = rawArgs.find((a) => a && !a.startsWith("--")) || "";
955
+ if (!key && !process.stdin.isTTY) {
956
+ try { key = fs.readFileSync(0, "utf8").trim(); } catch {}
957
+ }
958
+
959
+ key = String(key || "").trim();
960
+ if (!key) {
961
+ console.log(` ${RED}✗${RESET} Missing ERP API key.`);
962
+ console.log(` ${DIM}Usage:${RESET} qualia-framework set-erp-key <key>`);
963
+ console.log(` ${DIM}Safe shell history option:${RESET} printf '%s' "$QUALIA_ERP_KEY" | qualia-framework set-erp-key`);
964
+ console.log("");
965
+ process.exit(1);
966
+ }
967
+
968
+ if (key.length < 10) {
969
+ console.log(` ${YELLOW}!${RESET} Key looks short (${key.length} bytes). Saving anyway.`);
970
+ }
971
+
972
+ fs.writeFileSync(keyFile, key, { mode: 0o600 });
973
+ try { fs.chmodSync(keyFile, 0o600); } catch {}
974
+
975
+ const cfg = readConfig();
976
+ cfg.erp = {
977
+ ...(cfg.erp || {}),
978
+ enabled: true,
979
+ url: (cfg.erp && cfg.erp.url) || "https://portal.qualiasolutions.net",
980
+ api_key_file: ".erp-api-key",
981
+ };
982
+ writeConfig(cfg);
983
+
984
+ console.log(` ${GREEN}✓${RESET} ERP key saved to ${WHITE}${keyFile}${RESET}`);
985
+ console.log(` ${DIM}Verify with:${RESET} ${TEAL}qualia-framework erp-ping${RESET}`);
986
+ console.log("");
987
+ }
988
+
989
+ // ─── Doctor: post-install health check ───────────────────
990
+ // Mirrors the spot-check that session-start.js runs once per 24h. Surfaces
991
+ // missing files, mis-wired hooks, stale settings.json, and version drift.
992
+ // Use whenever something feels off, before opening an issue, or after a
993
+ // version upgrade. Exits 0 if healthy, 1 if any issue is found.
994
+ // ─── Flush: convenience wrapper around knowledge-flush.js ───────
995
+ // Exposes the cron-runnable script as a top-level CLI command so users can
996
+ // run `qualia-framework flush` ad-hoc. All args after the command pass through.
997
+ function cmdFlush() {
998
+ const flushScript = path.join(CLAUDE_DIR, "bin", "knowledge-flush.js");
999
+ if (!fs.existsSync(flushScript)) {
1000
+ console.log(` ${RED}✗${RESET} knowledge-flush.js not installed at ${flushScript}`);
1001
+ console.log(` ${DIM}Run: npx qualia-framework@latest install${RESET}`);
1002
+ process.exit(1);
1003
+ }
1004
+ const args = process.argv.slice(3);
1005
+ const r = spawnSync(process.execPath, [flushScript, ...args], {
1006
+ stdio: "inherit",
1007
+ shell: false,
1008
+ });
1009
+ process.exit(typeof r.status === "number" ? r.status : 1);
1010
+ }
1011
+
1012
+ function cmdDoctor() {
1013
+ banner();
1014
+ console.log("");
1015
+
1016
+ const issues = [];
1017
+ const checks = [];
1018
+
1019
+ function check(label, ok, hint) {
1020
+ checks.push({ label, ok, hint });
1021
+ if (!ok) issues.push({ label, hint });
1022
+ }
1023
+
1024
+ // ── Critical files (the same set session-start.js validates) ──
1025
+ const criticalFiles = [
1026
+ path.join(CLAUDE_DIR, "rules", "grounding.md"),
1027
+ path.join(CLAUDE_DIR, "rules", "security.md"),
1028
+ path.join(CLAUDE_DIR, "rules", "frontend.md"),
1029
+ path.join(CLAUDE_DIR, "rules", "deployment.md"),
1030
+ path.join(CLAUDE_DIR, "bin", "state.js"),
1031
+ path.join(CLAUDE_DIR, "bin", "qualia-ui.js"),
1032
+ path.join(CLAUDE_DIR, "bin", "statusline.js"),
1033
+ path.join(CLAUDE_DIR, "bin", "knowledge.js"),
1034
+ path.join(CLAUDE_DIR, "bin", "knowledge-flush.js"),
1035
+ path.join(CLAUDE_DIR, "CLAUDE.md"),
1036
+ CONFIG_FILE,
1037
+ ];
1038
+ for (const f of criticalFiles) {
1039
+ check(
1040
+ `${path.relative(CLAUDE_DIR, f) || f}`,
1041
+ fs.existsSync(f),
1042
+ "run: npx qualia-framework@latest install",
1043
+ );
1044
+ }
1045
+
1046
+ // ── Hooks ─────────────────────────────────────────────
1047
+ for (const h of QUALIA_HOOK_FILES) {
1048
+ check(
1049
+ `hooks/${h}`,
1050
+ fs.existsSync(path.join(CLAUDE_DIR, "hooks", h)),
1051
+ "reinstall: npx qualia-framework@latest install",
1052
+ );
1053
+ }
1054
+
1055
+ // ── Knowledge layer ────────────────────────────────────
1056
+ const knowledgeFiles = [
1057
+ path.join(CLAUDE_DIR, "knowledge", "agents.md"),
1058
+ path.join(CLAUDE_DIR, "knowledge", "index.md"),
1059
+ path.join(CLAUDE_DIR, "knowledge", "daily-log"),
1060
+ ];
1061
+ for (const f of knowledgeFiles) {
1062
+ check(
1063
+ `knowledge/${path.basename(f)}${fs.existsSync(f) && fs.statSync(f).isDirectory() ? "/" : ""}`,
1064
+ fs.existsSync(f),
1065
+ "reinstall to initialize the memory layer: npx qualia-framework@latest install",
1066
+ );
1067
+ }
1068
+
1069
+ // ── settings.json hook wiring ──────────────────────────
1070
+ const settingsPath = path.join(CLAUDE_DIR, "settings.json");
1071
+ if (fs.existsSync(settingsPath)) {
1072
+ try {
1073
+ const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
1074
+ const wantEvents = ["SessionStart", "PreToolUse", "PreCompact", "Stop"];
1075
+ for (const ev of wantEvents) {
1076
+ const blocks = (settings.hooks || {})[ev] || [];
1077
+ const hasQualia = blocks.some((b) =>
1078
+ (b.hooks || []).some((h) => typeof h.command === "string" && h.command.includes(".claude")),
1079
+ );
1080
+ check(`settings.json hooks.${ev}`, hasQualia, "reinstall to wire hooks");
1081
+ }
1082
+ } catch (e) {
1083
+ check("settings.json parseable", false, e.message);
1084
+ }
1085
+ } else {
1086
+ check("settings.json", false, "Claude Code never ran here? Open Claude once first");
1087
+ }
1088
+
1089
+ // ── Version vs. installed ──────────────────────────────
1090
+ const cfg = readConfig();
1091
+ if (cfg.installed_at) {
1092
+ check(`config installed_by=${cfg.installed_by || "?"} role=${cfg.role || "?"}`, true);
1093
+ } else {
1094
+ check("config has install metadata", false, "reinstall to record");
1095
+ }
1096
+
1097
+ // ── Render ────────────────────────────────────────────
1098
+ for (const c of checks) {
1099
+ const mark = c.ok ? `${GREEN}✓${RESET}` : `${RED}✗${RESET}`;
1100
+ console.log(` ${mark} ${c.label}`);
1101
+ }
1102
+ console.log("");
1103
+ if (issues.length === 0) {
1104
+ console.log(` ${GREEN}All checks passed. Framework is healthy.${RESET}`);
1105
+ console.log("");
1106
+ process.exit(0);
1107
+ }
1108
+ console.log(` ${RED}${issues.length} issue${issues.length === 1 ? "" : "s"} found:${RESET}`);
1109
+ for (const i of issues) {
1110
+ console.log(` ${DIM}•${RESET} ${i.label}${i.hint ? ` ${DIM}— ${i.hint}${RESET}` : ""}`);
1111
+ }
1112
+ console.log("");
1113
+ process.exit(1);
1114
+ }
1115
+
1116
+ // ─── Agents: per-spawn telemetry (see docs/agent-runs.md) ─────────
1117
+ function cmdAgents() {
1118
+ banner();
1119
+ console.log("");
1120
+
1121
+ // Lazy require so the CLI works even if the lib is missing during dev.
1122
+ let lib;
1123
+ try { lib = require("./agent-runs.js"); }
1124
+ catch (e) {
1125
+ console.log(` ${RED}✗${RESET} agent-runs.js not available: ${e.message}`);
1126
+ console.log("");
1127
+ process.exit(1);
1128
+ }
1129
+
1130
+ const args = process.argv.slice(3);
1131
+ const flags = new Set(args.filter((a) => a.startsWith("--")));
1132
+ const sub = args.find((a) => !a.startsWith("--"));
1133
+
1134
+ const cwd = process.cwd();
1135
+
1136
+ if (sub === "prune") {
1137
+ const idx = args.indexOf("--before");
1138
+ const before = idx >= 0 ? args[idx + 1] : null;
1139
+ if (!before) {
1140
+ console.log(` ${RED}✗${RESET} usage: qualia-framework agents prune --before YYYY-MM-DD`);
1141
+ console.log("");
1142
+ process.exit(1);
1143
+ }
1144
+ try {
1145
+ const r = lib.prune(cwd, before);
1146
+ console.log(` ${GREEN}✓${RESET} Pruned ${r.removed} run record(s), ${r.logs_removed} log file(s).`);
1147
+ console.log("");
1148
+ return;
1149
+ } catch (e) {
1150
+ console.log(` ${RED}✗${RESET} ${e.message}`);
1151
+ console.log("");
1152
+ process.exit(1);
1153
+ }
1154
+ }
1155
+
1156
+ const opts = { limit: 50 };
1157
+ if (flags.has("--failed")) opts.failed = true;
1158
+ const taskIdx = args.indexOf("--task");
1159
+ if (taskIdx >= 0) opts.task_id = args[taskIdx + 1];
1160
+ const phaseIdx = args.indexOf("--phase");
1161
+ if (phaseIdx >= 0) opts.phase = Number(args[phaseIdx + 1]);
1162
+
1163
+ const records = lib.read(cwd, opts);
1164
+ if (records.length === 0) {
1165
+ console.log(` ${DIM}No agent runs recorded yet${RESET} ${DIM}(or run from a project with .planning/)${RESET}`);
1166
+ console.log("");
1167
+ return;
1168
+ }
1169
+
1170
+ console.log(` ${WHITE}Agent runs${RESET} ${DIM}(showing ${records.length}, newest last)${RESET}`);
1171
+ console.log(` ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
1172
+ console.log(` ${DIM}TIME AGENT PHASE TASK STATUS DURATION NOTE${RESET}`);
1173
+ for (const r of records) {
1174
+ const t = (r.finished_at || r.started_at || "").slice(11, 16);
1175
+ const agent = (r.agent_type || "").padEnd(11);
1176
+ const phase = String(r.phase ?? "-").padEnd(5);
1177
+ const task = (r.task_id || "-").padEnd(4);
1178
+ const stColor = r.status === "success" ? GREEN : r.status === "partial" ? YELLOW : RED;
1179
+ const status = `${stColor}${(r.status || "?").padEnd(9)}${RESET}`;
1180
+ const dur = r.duration_ms != null ? `${Math.round(r.duration_ms / 1000)}s`.padStart(7) : " —";
1181
+ const note = r.failure_reason
1182
+ ? `${DIM}${r.failure_reason}${RESET}`
1183
+ : (r.commit_sha ? `${DIM}${r.commit_sha.slice(0, 7)}${RESET}` : "");
1184
+ console.log(` ${t} ${agent} ${phase} ${task} ${status} ${dur} ${note}`);
1185
+ }
1186
+ console.log("");
1187
+ const failed = records.filter((r) => r.status !== "success").length;
1188
+ if (failed > 0) {
1189
+ console.log(` ${YELLOW}${failed} non-success run(s).${RESET} ${DIM}qualia-framework agents --failed for details${RESET}`);
1190
+ console.log("");
1191
+ }
1192
+ }
1193
+
875
1194
  function cmdHelp() {
876
1195
  banner();
877
1196
  console.log("");
@@ -880,11 +1199,15 @@ function cmdHelp() {
880
1199
  console.log(` qualia-framework ${TEAL}update${RESET} Update to the latest version`);
881
1200
  console.log(` qualia-framework ${TEAL}version${RESET} Show installed version + check for updates`);
882
1201
  console.log(` qualia-framework ${TEAL}uninstall${RESET} Clean removal from ~/.claude/ (${DIM}-y to skip prompts${RESET})`);
883
- console.log(` qualia-framework ${TEAL}migrate${RESET} Migrate settings from v2 to v3`);
1202
+ console.log(` qualia-framework ${TEAL}migrate${RESET} Wire current hook + env layout into ~/.claude/settings.json`);
884
1203
  console.log(` qualia-framework ${TEAL}team${RESET} Manage team members (${DIM}list|add|remove${RESET})`);
885
1204
  console.log(` qualia-framework ${TEAL}traces${RESET} View recent hook telemetry`);
886
1205
  console.log(` qualia-framework ${TEAL}analytics${RESET} Show outcome scoring & gap cycle stats`);
1206
+ console.log(` qualia-framework ${TEAL}agents${RESET} Show per-agent run history (${DIM}--failed|--task ID|--phase N|prune --before${RESET})`);
1207
+ console.log(` qualia-framework ${TEAL}set-erp-key${RESET} Save/enable the ERP API key`);
887
1208
  console.log(` qualia-framework ${TEAL}erp-ping${RESET} Verify ERP connectivity + API key`);
1209
+ console.log(` qualia-framework ${TEAL}doctor${RESET} Health-check the install (files, hooks, settings)`);
1210
+ console.log(` qualia-framework ${TEAL}flush${RESET} Promote daily-log → curated knowledge (memory layer)`);
888
1211
  console.log("");
889
1212
  console.log(` ${WHITE}After install:${RESET}`);
890
1213
  console.log(` ${TG}/qualia${RESET} What should I do next?`);
@@ -933,14 +1256,30 @@ switch (cmd) {
933
1256
  case "migrate":
934
1257
  cmdMigrate();
935
1258
  break;
1259
+ case "agents":
1260
+ cmdAgents();
1261
+ break;
936
1262
  case "analytics":
937
1263
  case "stats":
938
1264
  cmdAnalytics();
939
1265
  break;
1266
+ case "set-erp-key":
1267
+ case "erp-key":
1268
+ cmdSetErpKey();
1269
+ break;
940
1270
  case "erp-ping":
941
1271
  case "ping":
942
1272
  cmdErpPing();
943
1273
  break;
1274
+ case "doctor":
1275
+ case "health":
1276
+ case "health-check":
1277
+ cmdDoctor();
1278
+ break;
1279
+ case "flush":
1280
+ case "knowledge-flush":
1281
+ cmdFlush();
1282
+ break;
944
1283
  default:
945
1284
  cmdHelp();
946
1285
  }
package/bin/install.js CHANGED
@@ -294,8 +294,12 @@ async function main() {
294
294
  const tmplDir = path.join(FRAMEWORK_DIR, "templates");
295
295
  const tmplDest = path.join(CLAUDE_DIR, "qualia-templates");
296
296
  if (!fs.existsSync(tmplDest)) fs.mkdirSync(tmplDest, { recursive: true });
297
+ // `knowledge/` is a sibling templates directory but installs to a different
298
+ // destination (~/.claude/knowledge/, not ~/.claude/qualia-templates/), so we
299
+ // skip it here and handle it in the dedicated "Knowledge layer" section below.
297
300
  for (const entry of fs.readdirSync(tmplDir, { withFileTypes: true })) {
298
301
  if (entry.name.startsWith(".")) continue;
302
+ if (entry.name === "knowledge") continue;
299
303
  const srcPath = path.join(tmplDir, entry.name);
300
304
  const destPath = path.join(tmplDest, entry.name);
301
305
  try {
@@ -311,6 +315,38 @@ async function main() {
311
315
  }
312
316
  }
313
317
 
318
+ // ─── Knowledge layer (Karpathy-style raw → wiki memory tier) ──────
319
+ // Initializes ~/.claude/knowledge/ on first install. Never overwrites
320
+ // existing files — re-running the installer is safe for users who have
321
+ // already accumulated learnings.
322
+ printSection("Knowledge layer");
323
+ const knowledgeSrc = path.join(FRAMEWORK_DIR, "templates", "knowledge");
324
+ const knowledgeDest = path.join(CLAUDE_DIR, "knowledge");
325
+ if (!fs.existsSync(knowledgeDest)) fs.mkdirSync(knowledgeDest, { recursive: true });
326
+ const dailyLogDir = path.join(knowledgeDest, "daily-log");
327
+ if (!fs.existsSync(dailyLogDir)) {
328
+ fs.mkdirSync(dailyLogDir, { recursive: true });
329
+ ok("daily-log/ (created)");
330
+ } else {
331
+ log(`${DIM}daily-log/ (kept)${RESET}`);
332
+ }
333
+ if (fs.existsSync(knowledgeSrc)) {
334
+ for (const file of fs.readdirSync(knowledgeSrc)) {
335
+ const src = path.join(knowledgeSrc, file);
336
+ const dest = path.join(knowledgeDest, file);
337
+ try {
338
+ if (fs.existsSync(dest)) {
339
+ log(`${DIM}${file} (kept — user has customized)${RESET}`);
340
+ } else {
341
+ copy(src, dest);
342
+ ok(`${file} (initialized)`);
343
+ }
344
+ } catch (e) {
345
+ warn(`${file} — ${e.message}`);
346
+ }
347
+ }
348
+ }
349
+
314
350
  // ─── References (methodology docs loaded by skills at runtime) ────
315
351
  printSection("References");
316
352
  const refDir = path.join(FRAMEWORK_DIR, "references");
@@ -374,6 +410,28 @@ async function main() {
374
410
  path.join(binDest, "statusline.js")
375
411
  );
376
412
  ok("statusline.js (status bar renderer)");
413
+ copy(
414
+ path.join(FRAMEWORK_DIR, "bin", "knowledge.js"),
415
+ path.join(binDest, "knowledge.js")
416
+ );
417
+ fs.chmodSync(path.join(binDest, "knowledge.js"), 0o755);
418
+ ok("knowledge.js (memory-layer loader)");
419
+ copy(
420
+ path.join(FRAMEWORK_DIR, "bin", "knowledge-flush.js"),
421
+ path.join(binDest, "knowledge-flush.js")
422
+ );
423
+ fs.chmodSync(path.join(binDest, "knowledge-flush.js"), 0o755);
424
+ ok("knowledge-flush.js (cron-runnable flush)");
425
+ copy(
426
+ path.join(FRAMEWORK_DIR, "bin", "plan-contract.js"),
427
+ path.join(binDest, "plan-contract.js")
428
+ );
429
+ ok("plan-contract.js (plan JSON validator)");
430
+ copy(
431
+ path.join(FRAMEWORK_DIR, "bin", "agent-runs.js"),
432
+ path.join(binDest, "agent-runs.js")
433
+ );
434
+ ok("agent-runs.js (agent telemetry writer)");
377
435
  } catch (e) {
378
436
  warn(`scripts — ${e.message}`);
379
437
  }
@@ -538,8 +596,8 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
538
596
  try { fs.chmodSync(configFile, 0o600); } catch {}
539
597
  } catch {}
540
598
  log(`${YELLOW}!${RESET} ERP key not configured — reports won't upload until set.`);
541
- log(`${DIM} Set with:${RESET} ${TEAL}export QUALIA_ERP_KEY=...${RESET} ${DIM}then re-install,${RESET}`);
542
- log(`${DIM} or write the key to:${RESET} ${WHITE}${erpKeyFile}${RESET} ${DIM}(mode 0600).${RESET}`);
599
+ log(`${DIM} Set with:${RESET} ${TEAL}qualia-framework set-erp-key <key>${RESET}`);
600
+ log(`${DIM} or:${RESET} ${TEAL}export QUALIA_ERP_KEY=...${RESET} ${DIM}then re-install.${RESET}`);
543
601
  log(`${DIM} Get a key from Fawzi.${RESET}`);
544
602
  }
545
603
 
@@ -562,6 +620,15 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
562
620
  CLAUDE_CODE_DISABLE_AUTO_MEMORY: "0",
563
621
  MAX_MCP_OUTPUT_TOKENS: "25000",
564
622
  CLAUDE_CODE_NO_FLICKER: "1",
623
+ // v4.2.0 phase 3 — enable forked subagents (Anthropic, 2026-04).
624
+ // Forks inherit the full conversation history + share the prompt cache,
625
+ // so design fan-outs and discuss-context handoffs preserve nuance instead
626
+ // of compressing 50k tokens of taste discussion into a 2k subagent prompt.
627
+ // /qualia-design and the builder agent reach for /fork when discuss
628
+ // context exists in the current session; verifier and plan-checker still
629
+ // use blank-context spawns to avoid the "kid grading their own homework"
630
+ // failure mode.
631
+ CLAUDE_AGENT_FORK_ENABLED: "1",
565
632
  });
566
633
 
567
634
  // Status line
@@ -613,6 +680,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
613
680
  const QUALIA_HOOK_SET = new Set([
614
681
  "session-start.js", "auto-update.js", "branch-guard.js", "pre-push.js",
615
682
  "pre-deploy-gate.js", "migration-guard.js", "pre-compact.js",
683
+ "git-guardrails.js", "stop-session-log.js",
616
684
  ]);
617
685
  const isQualiaHookCmd = (cmd) => {
618
686
  if (typeof cmd !== "string") return false;
@@ -635,6 +703,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
635
703
  matcher: "Bash",
636
704
  hooks: [
637
705
  { type: "command", command: nodeCmd("auto-update.js"), timeout: 5 },
706
+ { type: "command", command: nodeCmd("git-guardrails.js"), timeout: 5, statusMessage: "⬢ Checking git safety..." },
638
707
  { type: "command", if: "Bash(git push*)", command: nodeCmd("branch-guard.js"), timeout: 5, statusMessage: "⬢ Checking branch permissions..." },
639
708
  { type: "command", if: "Bash(git push*)", command: nodeCmd("pre-push.js"), timeout: 15, statusMessage: "⬢ Syncing tracking..." },
640
709
  { type: "command", if: "Bash(vercel --prod*)", command: nodeCmd("pre-deploy-gate.js"), timeout: 180, statusMessage: "⬢ Running quality gates..." },
@@ -655,6 +724,14 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
655
724
  ],
656
725
  },
657
726
  ],
727
+ Stop: [
728
+ {
729
+ matcher: ".*",
730
+ hooks: [
731
+ { type: "command", command: nodeCmd("stop-session-log.js"), timeout: 5 },
732
+ ],
733
+ },
734
+ ],
658
735
  };
659
736
 
660
737
  // Merge user hooks: strip Qualia-owned commands, preserve everything else.
@@ -672,8 +749,8 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
672
749
  settings.hooks[event] = [...cleaned, ...qualiaHooks[event]];
673
750
  }
674
751
 
675
- // Permissions no restrictions on env files or branches.
676
- // Everyone can read/write .env, push to main.
752
+ // Permissions stay permissive; Qualia policy enforcement happens in hooks so
753
+ // OWNER overrides and EMPLOYEE blocks can share one source of truth.
677
754
  if (!settings.permissions) settings.permissions = {};
678
755
  if (!settings.permissions.allow) settings.permissions.allow = [];
679
756
  if (!settings.permissions.deny) settings.permissions.deny = [];
@@ -692,7 +769,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
692
769
 
693
770
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
694
771
 
695
- ok("Hooks: session-start, auto-update, branch-guard, pre-push, migration-guard, deploy-gate, pre-compact");
772
+ ok("Hooks: session-start, auto-update, branch-guard, pre-push, migration-guard, deploy-gate, pre-compact, git-guardrails, stop-session-log");
696
773
  ok("Status line + spinner configured");
697
774
  ok("Environment variables + permissions");
698
775
 
@@ -709,7 +786,11 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
709
786
  const ruleCount = fs.readdirSync(rulesDir).length;
710
787
  const tmplCount = fs.readdirSync(tmplDir).length;
711
788
  console.log(` ${DIM}Skills${RESET} ${TEAL}${skills.length}${RESET} ${DIM}Agents${RESET} ${TEAL}${agentCount}${RESET} ${DIM}Hooks${RESET} ${TEAL}${hookCount}${RESET}`);
712
- console.log(` ${DIM}Rules${RESET} ${TEAL}${ruleCount}${RESET} ${DIM}Scripts${RESET} ${TEAL}3${RESET} ${DIM}Templates${RESET} ${TEAL}${tmplCount}${RESET}`);
789
+ const installedBinDir = path.join(CLAUDE_DIR, "bin");
790
+ const scriptCount = fs.existsSync(installedBinDir)
791
+ ? fs.readdirSync(installedBinDir).filter(f => f.endsWith('.js')).length
792
+ : 0;
793
+ console.log(` ${DIM}Rules${RESET} ${TEAL}${ruleCount}${RESET} ${DIM}Scripts${RESET} ${TEAL}${scriptCount}${RESET} ${DIM}Templates${RESET} ${TEAL}${tmplCount}${RESET}`);
713
794
 
714
795
  if (errors > 0) {
715
796
  console.log("");