qualia-framework 6.2.9 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/AGENTS.md +1 -0
  2. package/CLAUDE.md +1 -0
  3. package/README.md +26 -30
  4. package/agents/builder.md +7 -7
  5. package/agents/planner.md +39 -3
  6. package/agents/research-synthesizer.md +1 -1
  7. package/agents/researcher.md +3 -3
  8. package/agents/roadmapper.md +7 -7
  9. package/agents/verifier.md +18 -6
  10. package/agents/visual-evaluator.md +8 -7
  11. package/bin/cli.js +160 -16
  12. package/bin/command-surface.js +71 -0
  13. package/bin/contract-runner.js +219 -0
  14. package/bin/harness-eval.js +296 -0
  15. package/bin/host-adapters.js +66 -0
  16. package/bin/install.js +116 -172
  17. package/bin/knowledge-flush.js +21 -10
  18. package/bin/knowledge.js +1 -1
  19. package/bin/plan-contract.js +99 -2
  20. package/bin/planning-hygiene.js +262 -0
  21. package/bin/project-snapshot.js +20 -0
  22. package/bin/report-payload.js +18 -0
  23. package/bin/runtime-manifest.js +35 -0
  24. package/bin/state-ledger.js +184 -0
  25. package/bin/state.js +330 -20
  26. package/bin/trust-score.js +268 -0
  27. package/bin/work-packet.js +228 -0
  28. package/docs/erp-contract.md +81 -1
  29. package/docs/onboarding.html +4 -14
  30. package/guide.md +16 -16
  31. package/hooks/fawzi-approval-guard.js +143 -0
  32. package/hooks/pre-deploy-gate.js +74 -1
  33. package/hooks/session-start.js +29 -1
  34. package/package.json +1 -1
  35. package/qualia-design/design-rubric.md +17 -5
  36. package/qualia-design/frontend.md +6 -2
  37. package/qualia-design/graphics.md +47 -0
  38. package/rules/codex-goal.md +1 -1
  39. package/rules/command-output.md +35 -0
  40. package/rules/one-opinion.md +2 -2
  41. package/rules/speed.md +0 -1
  42. package/skills/qualia/SKILL.md +12 -12
  43. package/skills/qualia-build/SKILL.md +20 -14
  44. package/skills/qualia-discuss/SKILL.md +10 -10
  45. package/skills/qualia-doctor/SKILL.md +140 -0
  46. package/skills/qualia-feature/SKILL.md +24 -22
  47. package/skills/qualia-fix/SKILL.md +216 -0
  48. package/skills/qualia-handoff/SKILL.md +9 -9
  49. package/skills/qualia-learn/SKILL.md +11 -11
  50. package/skills/qualia-map/SKILL.md +2 -2
  51. package/skills/qualia-milestone/SKILL.md +15 -15
  52. package/skills/qualia-new/REFERENCE.md +9 -9
  53. package/skills/qualia-new/SKILL.md +14 -14
  54. package/skills/qualia-optimize/REFERENCE.md +1 -1
  55. package/skills/qualia-optimize/SKILL.md +23 -16
  56. package/skills/qualia-plan/SKILL.md +23 -13
  57. package/skills/qualia-polish/REFERENCE.md +15 -15
  58. package/skills/qualia-polish/SKILL.md +81 -21
  59. package/skills/qualia-polish/scripts/loop.mjs +3 -3
  60. package/skills/qualia-polish/scripts/score.mjs +9 -3
  61. package/skills/{qualia-vibe/scripts/extract.mjs → qualia-polish/scripts/vibe-extract.mjs} +5 -5
  62. package/skills/{qualia-vibe/scripts/tokens.mjs → qualia-polish/scripts/vibe-tokens.mjs} +6 -6
  63. package/skills/qualia-postmortem/SKILL.md +9 -9
  64. package/skills/qualia-report/SKILL.md +23 -23
  65. package/skills/qualia-research/SKILL.md +5 -5
  66. package/skills/qualia-review/SKILL.md +28 -12
  67. package/skills/qualia-road/SKILL.md +30 -22
  68. package/skills/qualia-ship/SKILL.md +31 -24
  69. package/skills/qualia-test/SKILL.md +5 -5
  70. package/skills/qualia-verify/SKILL.md +45 -23
  71. package/skills/zoho-workflow/SKILL.md +1 -1
  72. package/templates/help.html +11 -20
  73. package/tests/bin.test.sh +178 -76
  74. package/tests/hooks.test.sh +81 -1
  75. package/tests/install-smoke.test.sh +35 -5
  76. package/tests/lib.test.sh +432 -0
  77. package/tests/published-install-smoke.test.sh +4 -3
  78. package/tests/refs.test.sh +9 -4
  79. package/tests/runner.js +32 -28
  80. package/tests/skills.test.sh +4 -4
  81. package/tests/state.test.sh +133 -3
  82. package/skills/qualia-debug/SKILL.md +0 -185
  83. package/skills/qualia-flush/SKILL.md +0 -198
  84. package/skills/qualia-help/SKILL.md +0 -74
  85. package/skills/qualia-hook-gen/SKILL.md +0 -206
  86. package/skills/qualia-idk/SKILL.md +0 -166
  87. package/skills/qualia-issues/SKILL.md +0 -151
  88. package/skills/qualia-pause/SKILL.md +0 -68
  89. package/skills/qualia-resume/SKILL.md +0 -52
  90. package/skills/qualia-skill-new/SKILL.md +0 -173
  91. package/skills/qualia-triage/SKILL.md +0 -152
  92. package/skills/qualia-vibe/SKILL.md +0 -226
  93. package/skills/qualia-zoom/SKILL.md +0 -51
package/bin/install.js CHANGED
@@ -4,6 +4,9 @@ const { createInterface } = require("readline");
4
4
  const path = require("path");
5
5
  const fs = require("fs");
6
6
  const ui = require("./qualia-ui.js");
7
+ const { RUNTIME_BIN_SCRIPTS, binFiles } = require("./runtime-manifest.js");
8
+ const { ACTIVE_SKILLS, RETIRED_SKILLS } = require("./command-surface.js");
9
+ const { renderText } = require("./host-adapters.js");
7
10
 
8
11
  // ─── Colors (kept for legacy log lines; new sections route through qualia-ui) ─
9
12
  const TEAL = "\x1b[38;2;0;206;209m";
@@ -24,12 +27,30 @@ const TARGET_CLAUDE_ONLY = "1";
24
27
  const TARGET_CODEX_ONLY = "2";
25
28
  const TARGET_BOTH = "3";
26
29
 
30
+ const CODEX_STATUS_LINE = [
31
+ "model-with-reasoning",
32
+ "task-progress",
33
+ "current-dir",
34
+ "git-branch",
35
+ "context-used",
36
+ "five-hour-limit",
37
+ "weekly-limit",
38
+ ];
39
+
40
+ const CODEX_STATUS_LINE_BLOCK = [
41
+ "# Added by qualia-framework — Codex native bottom status line.",
42
+ "[tui]",
43
+ `status_line = ${JSON.stringify(CODEX_STATUS_LINE)}`,
44
+ "status_line_use_colors = true",
45
+ "",
46
+ ].join("\n");
47
+
27
48
  // Total install timer — set in main(), read by the final summary card.
28
49
  const installStart = Date.now();
29
50
 
30
51
  // ─── Team codes ──────────────────────────────────────────
31
52
  const DEFAULT_TEAM = {
32
- "QS-FAWZI-01": {
53
+ "QS-FAWZI-11": {
33
54
  name: "Fawzi Goussous",
34
55
  role: "OWNER",
35
56
  description: "Company owner. Full access. Can push to main, approve deploys, edit secrets.",
@@ -125,14 +146,8 @@ function copyTree(src, dest) {
125
146
  }
126
147
  }
127
148
 
128
- function codexText(content) {
129
- return String(content)
130
- .replaceAll("~/.claude/", "~/.codex/")
131
- .replaceAll("$HOME/.claude/", "$HOME/.codex/")
132
- .replaceAll("${HOME}/.claude/", "${HOME}/.codex/")
133
- .replaceAll("@~/.claude/", "@~/.codex/")
134
- .replaceAll(".claude/", ".codex/");
135
- }
149
+ const claudeText = (content) => renderText(content, "claude");
150
+ const codexText = (content) => renderText(content, "codex");
136
151
 
137
152
  function copyTextTransform(src, dest, transform) {
138
153
  const destDir = path.dirname(dest);
@@ -156,6 +171,45 @@ function copyTreeTransform(src, dest, transform) {
156
171
  }
157
172
  }
158
173
 
174
+ function ensureCodexStatusLineConfig(existing) {
175
+ const statusLine = `status_line = ${JSON.stringify(CODEX_STATUS_LINE)}`;
176
+ const colors = "status_line_use_colors = true";
177
+ if (!existing || !existing.trim()) {
178
+ return [
179
+ "# Created by qualia-framework install.",
180
+ "# User settings can be added normally; Qualia runtime lives in AGENTS.md, hooks.json, agents/, and bin/.",
181
+ "",
182
+ "[features]",
183
+ "hooks = true",
184
+ "plugin_hooks = true",
185
+ "",
186
+ "# Codex's native status line is rendered at the bottom of the TUI.",
187
+ "# It supports a fixed list of built-in segment names. Custom command-backed",
188
+ "# status lines are not supported in Codex 0.133, so Qualia phase/project",
189
+ "# context is rendered by the SessionStart banner while the native bottom",
190
+ "# line keeps model, task, directory, git, context, and limit state visible.",
191
+ CODEX_STATUS_LINE_BLOCK,
192
+ ].join("\n");
193
+ }
194
+
195
+ let next = existing;
196
+ if (!/^\[tui\]\s*$/m.test(next)) {
197
+ return `${next.replace(/\s*$/, "\n\n")}${CODEX_STATUS_LINE_BLOCK}`;
198
+ }
199
+
200
+ const tuiMatch = next.match(/^\[tui\]\s*$(?:\n(?!\[)[^\n]*)*/m);
201
+ if (!tuiMatch) return `${next.replace(/\s*$/, "\n\n")}${CODEX_STATUS_LINE_BLOCK}`;
202
+
203
+ let tuiBlock = tuiMatch[0];
204
+ if (!/^\s*status_line\s*=/m.test(tuiBlock)) {
205
+ tuiBlock = tuiBlock.replace(/^\[tui\]\s*$/m, `[tui]\n${statusLine}`);
206
+ }
207
+ if (!/^\s*status_line_use_colors\s*=/m.test(tuiBlock)) {
208
+ tuiBlock = `${tuiBlock.replace(/\s*$/, "")}\n${colors}`;
209
+ }
210
+ return `${next.slice(0, tuiMatch.index)}${tuiBlock}${next.slice(tuiMatch.index + tuiMatch[0].length)}`;
211
+ }
212
+
159
213
  function backupIfDifferent(dest, nextContent, label) {
160
214
  if (!fs.existsSync(dest)) return false;
161
215
  try {
@@ -207,9 +261,7 @@ function parseAgentMarkdown(content) {
207
261
 
208
262
  function renderCodexAgentToml(markdown, filenameFallback) {
209
263
  const parsed = parseAgentMarkdown(markdown);
210
- const body = parsed.body
211
- .replaceAll("~/.claude/", "~/.codex/")
212
- .replaceAll("@~/.claude/", "@~/.codex/");
264
+ const body = codexText(parsed.body);
213
265
  const name = (parsed.name || filenameFallback || "").replace(/^qualia-/, "");
214
266
  const description = parsed.description || "Qualia Framework specialist agent.";
215
267
  return [
@@ -224,11 +276,7 @@ function renderCodexAgentToml(markdown, filenameFallback) {
224
276
  // Pruned from BOTH ~/.claude/skills/ and ~/.codex/skills/ on every install run
225
277
  // so the active surface matches what the framework currently ships.
226
278
  const DEPRECATED_SKILLS = [
227
- "qualia-task", // v5.7.0 — folded into qualia-feature
228
- "qualia-quick", // v5.7.0 — folded into qualia-feature
229
- "qualia-polish-loop", // v5.8.0 — folded into qualia-polish --loop
230
- "qualia-design", // v4 wave 2 — folded into scope-adaptive qualia-polish
231
- "qualia-prd", // v5.8.0 — surface cleanup
279
+ ...RETIRED_SKILLS,
232
280
  ];
233
281
 
234
282
  function pruneDeprecatedSkills(baseDir) {
@@ -440,8 +488,8 @@ function targetLabel(t) {
440
488
  }
441
489
 
442
490
  // ─── Resolve team code (tolerates case + O/0 typo in suffix) ─
443
- // Accepts "qs-fawzi-01", "QS-FAWZI-01", "QS-FAWZI-O1" (letter O in the
444
- // numeric suffix), and returns the canonical key if found, else null.
491
+ // Accepts lowercase codes and common letter-O typos in numeric suffixes,
492
+ // then returns the canonical key if found, else null.
445
493
  // Only normalizes O→0 in the segment AFTER the last dash — "QS-MOAYAD-03"
446
494
  // contains a real "O" in the name and must not be mangled.
447
495
  function resolveTeamCode(input) {
@@ -470,7 +518,7 @@ async function main() {
470
518
  if (!member) {
471
519
  console.log("");
472
520
  log(`${RED}✗${RESET} Invalid code: "${rawCode}". Get your install code from Fawzi.`);
473
- log(`${DIM} Tip: codes use digit zero, not letter O. Format: QS-NAME-01${RESET}`);
521
+ log(`${DIM} Tip: codes use digit zero, not letter O. Format: QS-NAME-##${RESET}`);
474
522
  console.log("");
475
523
  process.exit(1);
476
524
  }
@@ -498,23 +546,22 @@ async function main() {
498
546
 
499
547
  // ─── Skills ──────────────────────────────────────────
500
548
  const skillsDir = path.join(FRAMEWORK_DIR, "skills");
501
- const skills = fs
502
- .readdirSync(skillsDir)
503
- .filter((d) => fs.statSync(path.join(skillsDir, d)).isDirectory());
549
+ const skills = ACTIVE_SKILLS.filter((d) => fs.existsSync(path.join(skillsDir, d, "SKILL.md")));
504
550
 
505
551
  printSection("Skills");
506
552
  const claudePruned = pruneDeprecatedSkills(CLAUDE_DIR);
507
553
  for (const name of claudePruned) ok(`pruned deprecated: ${name}`);
508
554
  for (const skill of skills) {
509
555
  try {
510
- copy(
556
+ copyTextTransform(
511
557
  path.join(skillsDir, skill, "SKILL.md"),
512
- path.join(CLAUDE_DIR, "skills", skill, "SKILL.md")
558
+ path.join(CLAUDE_DIR, "skills", skill, "SKILL.md"),
559
+ claudeText
513
560
  );
514
561
  // Copy REFERENCE.md if the skill has one (progressive-disclosure pattern)
515
562
  const refSrc = path.join(skillsDir, skill, "REFERENCE.md");
516
563
  if (fs.existsSync(refSrc)) {
517
- copy(refSrc, path.join(CLAUDE_DIR, "skills", skill, "REFERENCE.md"));
564
+ copyTextTransform(refSrc, path.join(CLAUDE_DIR, "skills", skill, "REFERENCE.md"), claudeText);
518
565
  }
519
566
  // v5.1: Copy scripts/ subfolder if present (e.g. qualia-polish ships
520
567
  // playwright-capture.mjs, loop.mjs, score.mjs that the --loop mode
@@ -537,7 +584,7 @@ async function main() {
537
584
  const agentsDir = path.join(FRAMEWORK_DIR, "agents");
538
585
  for (const file of fs.readdirSync(agentsDir)) {
539
586
  try {
540
- copy(path.join(agentsDir, file), path.join(CLAUDE_DIR, "agents", file));
587
+ copyTextTransform(path.join(agentsDir, file), path.join(CLAUDE_DIR, "agents", file), claudeText);
541
588
  ok(file);
542
589
  } catch (e) {
543
590
  warn(`${file} — ${e.message}`);
@@ -549,7 +596,7 @@ async function main() {
549
596
  const rulesDir = path.join(FRAMEWORK_DIR, "rules");
550
597
  for (const file of fs.readdirSync(rulesDir)) {
551
598
  try {
552
- copy(path.join(rulesDir, file), path.join(CLAUDE_DIR, "rules", file));
599
+ copyTextTransform(path.join(rulesDir, file), path.join(CLAUDE_DIR, "rules", file), claudeText);
553
600
  ok(file);
554
601
  } catch (e) {
555
602
  warn(`${file} — ${e.message}`);
@@ -590,7 +637,7 @@ async function main() {
590
637
  if (fs.existsSync(designDir)) {
591
638
  for (const file of fs.readdirSync(designDir)) {
592
639
  try {
593
- copy(path.join(designDir, file), path.join(designDest, file));
640
+ copyTextTransform(path.join(designDir, file), path.join(designDest, file), claudeText);
594
641
  ok(file);
595
642
  } catch (e) {
596
643
  warn(`${file} — ${e.message}`);
@@ -651,10 +698,10 @@ async function main() {
651
698
  const destPath = path.join(tmplDest, entry.name);
652
699
  try {
653
700
  if (entry.isDirectory()) {
654
- copyTree(srcPath, destPath);
701
+ copyTreeTransform(srcPath, destPath, claudeText);
655
702
  ok(`${entry.name}/ (directory)`);
656
703
  } else {
657
- copy(srcPath, destPath);
704
+ copyTextTransform(srcPath, destPath, claudeText);
658
705
  ok(entry.name);
659
706
  }
660
707
  } catch (e) {
@@ -685,7 +732,7 @@ async function main() {
685
732
  if (fs.existsSync(dest)) {
686
733
  log(`${DIM}${file} (kept — user has customized)${RESET}`);
687
734
  } else {
688
- copy(src, dest);
735
+ copyTextTransform(src, dest, claudeText);
689
736
  ok(`${file} (initialized)`);
690
737
  }
691
738
  } catch (e) {
@@ -702,7 +749,7 @@ async function main() {
702
749
  if (!fs.existsSync(refDest)) fs.mkdirSync(refDest, { recursive: true });
703
750
  for (const file of fs.readdirSync(refDir)) {
704
751
  try {
705
- copy(path.join(refDir, file), path.join(refDest, file));
752
+ copyTextTransform(path.join(refDir, file), path.join(refDest, file), claudeText);
706
753
  ok(file);
707
754
  } catch (e) {
708
755
  warn(`${file} — ${e.message}`);
@@ -741,7 +788,7 @@ async function main() {
741
788
  try { fs.copyFileSync(claudeDest, bak); ok(`Backed up existing CLAUDE.md → ${path.basename(bak)}`); } catch {}
742
789
  }
743
790
  }
744
- fs.writeFileSync(claudeDest, claudeMd, "utf8");
791
+ fs.writeFileSync(claudeDest, claudeText(claudeMd), "utf8");
745
792
  ok(`Configured as ${member.role}`);
746
793
  } catch (e) {
747
794
  warn(`CLAUDE.md — ${e.message}`);
@@ -752,84 +799,23 @@ async function main() {
752
799
  try {
753
800
  const binDest = path.join(CLAUDE_DIR, "bin");
754
801
  if (!fs.existsSync(binDest)) fs.mkdirSync(binDest, { recursive: true });
755
- copy(
756
- path.join(FRAMEWORK_DIR, "bin", "state.js"),
757
- path.join(binDest, "state.js")
758
- );
759
- ok("state.js (state machine)");
760
- copy(
761
- path.join(FRAMEWORK_DIR, "bin", "qualia-ui.js"),
762
- path.join(binDest, "qualia-ui.js")
763
- );
764
- fs.chmodSync(path.join(binDest, "qualia-ui.js"), 0o755);
765
- ok("qualia-ui.js (cosmetics library)");
766
- copy(
767
- path.join(FRAMEWORK_DIR, "bin", "statusline.js"),
768
- path.join(binDest, "statusline.js")
769
- );
770
- ok("statusline.js (status bar renderer)");
771
- copy(
772
- path.join(FRAMEWORK_DIR, "bin", "knowledge.js"),
773
- path.join(binDest, "knowledge.js")
774
- );
775
- fs.chmodSync(path.join(binDest, "knowledge.js"), 0o755);
776
- ok("knowledge.js (memory-layer loader)");
777
- copy(
778
- path.join(FRAMEWORK_DIR, "bin", "knowledge-flush.js"),
779
- path.join(binDest, "knowledge-flush.js")
780
- );
781
- fs.chmodSync(path.join(binDest, "knowledge-flush.js"), 0o755);
782
- ok("knowledge-flush.js (cron-runnable flush)");
783
- copy(
784
- path.join(FRAMEWORK_DIR, "bin", "plan-contract.js"),
785
- path.join(binDest, "plan-contract.js")
786
- );
787
- ok("plan-contract.js (plan JSON validator)");
788
- copy(
789
- path.join(FRAMEWORK_DIR, "bin", "agent-runs.js"),
790
- path.join(binDest, "agent-runs.js")
791
- );
792
- ok("agent-runs.js (agent telemetry writer)");
793
- copy(
794
- path.join(FRAMEWORK_DIR, "bin", "slop-detect.mjs"),
795
- path.join(binDest, "slop-detect.mjs")
796
- );
797
- fs.chmodSync(path.join(binDest, "slop-detect.mjs"), 0o755);
798
- ok("slop-detect.mjs (anti-pattern scanner — runs pre-commit on frontend builds)");
799
- copy(
800
- path.join(FRAMEWORK_DIR, "bin", "erp-retry.js"),
801
- path.join(binDest, "erp-retry.js")
802
- );
803
- fs.chmodSync(path.join(binDest, "erp-retry.js"), 0o755);
804
- ok("erp-retry.js (ERP report retry queue — drained by session-start hook and erp-flush CLI)");
805
- copy(
806
- path.join(FRAMEWORK_DIR, "bin", "report-payload.js"),
807
- path.join(binDest, "report-payload.js")
808
- );
809
- fs.chmodSync(path.join(binDest, "report-payload.js"), 0o755);
810
- ok("report-payload.js (Framework -> ERP report payload builder)");
811
- copy(
812
- path.join(FRAMEWORK_DIR, "bin", "project-snapshot.js"),
813
- path.join(binDest, "project-snapshot.js")
814
- );
815
- fs.chmodSync(path.join(binDest, "project-snapshot.js"), 0o755);
816
- ok("project-snapshot.js (ERP/admin project progress snapshot)");
817
- copy(
818
- path.join(FRAMEWORK_DIR, "bin", "codex-goal.js"),
819
- path.join(binDest, "codex-goal.js")
820
- );
821
- fs.chmodSync(path.join(binDest, "codex-goal.js"), 0o755);
822
- ok("codex-goal.js (Codex /goal objective + token-budget suggester)");
802
+ for (const script of RUNTIME_BIN_SCRIPTS) {
803
+ const out = path.join(binDest, script.file);
804
+ copy(path.join(FRAMEWORK_DIR, "bin", script.file), out);
805
+ try { fs.chmodSync(out, 0o755); } catch {}
806
+ ok(script.label);
807
+ }
823
808
  } catch (e) {
824
809
  warn(`scripts — ${e.message}`);
825
810
  }
826
811
 
827
812
  // ─── Guide ─────────────────────────────────────────────
828
813
  try {
829
- copy(
830
- path.join(FRAMEWORK_DIR, "guide.md"),
831
- path.join(CLAUDE_DIR, "qualia-guide.md")
832
- );
814
+ copyTextTransform(
815
+ path.join(FRAMEWORK_DIR, "guide.md"),
816
+ path.join(CLAUDE_DIR, "qualia-guide.md"),
817
+ claudeText
818
+ );
833
819
  ok("guide.md");
834
820
  } catch (e) {
835
821
  warn(`guide.md — ${e.message}`);
@@ -877,7 +863,7 @@ Recurring issues and their solutions.
877
863
  ## Install code "Invalid" — user typed letter O instead of digit 0
878
864
  **Symptom:** \`npx qualia-framework install\` rejects \`QS-NAME-O1\` (letter O in suffix).
879
865
  **Cause:** Team codes use digit zero (\`-01\`, \`-02\`, etc.), not letter O.
880
- **Fix:** Since v2.8.1, install.js auto-normalizes: \`QS-FAWZI-O1\` → \`QS-FAWZI-01\`. The normalization only touches the segment after the last dash, so \`QS-MOAYAD-03\` (real O in name) is preserved.
866
+ **Fix:** Since v2.8.1, install.js auto-normalizes: \`QS-HASAN-O2\` → \`QS-HASAN-02\`. The normalization only touches the segment after the last dash, so \`QS-MOAYAD-03\` (real O in name) is preserved.
881
867
  **Framework version:** Fixed in v2.8.1.
882
868
 
883
869
  ---
@@ -1048,6 +1034,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
1048
1034
  excludeDefault: true,
1049
1035
  tips: [
1050
1036
  "⬢ Lost? Type /qualia for the next step",
1037
+ "⬢ Broken behavior? Use /qualia-fix for root cause, patch, verify",
1051
1038
  "⬢ Single feature? Use /qualia-feature, it auto-scopes",
1052
1039
  "⬢ End of day? /qualia-report submits your shift before clock-out",
1053
1040
  "⬢ Context isolation: every task gets a fresh AI brain",
@@ -1069,6 +1056,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
1069
1056
  "session-start.js", "auto-update.js", "branch-guard.js", "pre-push.js",
1070
1057
  "pre-deploy-gate.js", "migration-guard.js", "pre-compact.js",
1071
1058
  "git-guardrails.js", "stop-session-log.js",
1059
+ "fawzi-approval-guard.js",
1072
1060
  // v5.0 — insights-driven destructive-op + wrong-account guards
1073
1061
  "vercel-account-guard.js", "env-empty-guard.js", "supabase-destructive-guard.js",
1074
1062
  ]);
@@ -1094,6 +1082,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
1094
1082
  hooks: [
1095
1083
  { type: "command", command: nodeCmd("auto-update.js"), timeout: 5 },
1096
1084
  { type: "command", command: nodeCmd("git-guardrails.js"), timeout: 5, statusMessage: "⬢ Checking git safety..." },
1085
+ { type: "command", command: nodeCmd("fawzi-approval-guard.js"), timeout: 5 },
1097
1086
  { type: "command", if: "Bash(git push*)", command: nodeCmd("branch-guard.js"), timeout: 5, statusMessage: "⬢ Checking branch permissions..." },
1098
1087
  { type: "command", if: "Bash(git push*)", command: nodeCmd("pre-push.js"), timeout: 15, statusMessage: "⬢ Syncing tracking..." },
1099
1088
  { type: "command", if: "Bash(vercel --prod*)", command: nodeCmd("pre-deploy-gate.js"), timeout: 180, statusMessage: "⬢ Running quality gates..." },
@@ -1106,6 +1095,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
1106
1095
  {
1107
1096
  matcher: "Edit|Write",
1108
1097
  hooks: [
1098
+ { type: "command", command: nodeCmd("fawzi-approval-guard.js"), timeout: 5 },
1109
1099
  { type: "command", if: "Edit(*migration*)|Write(*migration*)|Edit(*.sql)|Write(*.sql)", command: nodeCmd("migration-guard.js"), timeout: 10, statusMessage: "⬢ Checking migration safety..." },
1110
1100
  ],
1111
1101
  },
@@ -1179,7 +1169,7 @@ Client-specific preferences, design choices, and requirements. Loaded by \`/qual
1179
1169
  fs.writeFileSync(settingsTmp, JSON.stringify(settings, null, 2));
1180
1170
  fs.renameSync(settingsTmp, settingsPath);
1181
1171
 
1182
- ok("Hooks: session-start, auto-update, branch-guard, pre-push, migration-guard, deploy-gate, git-guardrails, stop-session-log, vercel-account-guard, env-empty-guard, supabase-destructive-guard");
1172
+ ok("Hooks: session-start, auto-update, branch-guard, pre-push, migration-guard, deploy-gate, git-guardrails, stop-session-log, fawzi-approval-guard, vercel-account-guard, env-empty-guard, supabase-destructive-guard");
1183
1173
  ok("Status line + spinner configured");
1184
1174
  ok("Environment variables + permissions");
1185
1175
 
@@ -1216,10 +1206,7 @@ function printSummary({ member, target, claudeInstalled }) {
1216
1206
  const hooksSource = path.join(FRAMEWORK_DIR, "hooks");
1217
1207
  const rulesDir = path.join(FRAMEWORK_DIR, "rules");
1218
1208
  const tmplDir = path.join(FRAMEWORK_DIR, "templates");
1219
- const skillsDir = path.join(FRAMEWORK_DIR, "skills");
1220
- const skillCount = fs
1221
- .readdirSync(skillsDir)
1222
- .filter((d) => fs.statSync(path.join(skillsDir, d)).isDirectory()).length;
1209
+ const skillCount = ACTIVE_SKILLS.length;
1223
1210
  const agentCount = fs.readdirSync(agentsDir).filter((f) => f.endsWith(".md")).length;
1224
1211
  const hookCount = fs.readdirSync(hooksSource).length;
1225
1212
  const ruleCount = fs.readdirSync(rulesDir).length;
@@ -1264,6 +1251,7 @@ function printSummary({ member, target, claudeInstalled }) {
1264
1251
  }
1265
1252
  console.log("");
1266
1253
  console.log(` ${DIM}New project?${RESET} ${TEAL}/qualia-new${RESET}`);
1254
+ console.log(` ${DIM}Broken thing?${RESET} ${TEAL}/qualia-fix${RESET}`);
1267
1255
  console.log(` ${DIM}Single feature?${RESET} ${TEAL}/qualia-feature${RESET}`);
1268
1256
  console.log(` ${DIM}End of day?${RESET} ${TEAL}/qualia-report${RESET} ${DIM}(shift submission)${RESET}`);
1269
1257
  console.log(` ${DIM}Stuck?${RESET} ${TEAL}/qualia${RESET}`);
@@ -1341,53 +1329,20 @@ async function installCodex(member, target) {
1341
1329
  }
1342
1330
 
1343
1331
  // Codex treats config.toml as optional, but doctor reports a warning when it
1344
- // is absent. Create a minimal, parseable file on fresh Codex-only homes and
1345
- // leave existing user config untouched.
1332
+ // is absent. Keep user settings intact while guaranteeing the native bottom
1333
+ // status line is present.
1346
1334
  try {
1347
1335
  const configToml = path.join(CODEX_DIR, "config.toml");
1336
+ const existing = fs.existsSync(configToml) ? fs.readFileSync(configToml, "utf8") : "";
1337
+ const next = ensureCodexStatusLineConfig(existing);
1348
1338
  if (!fs.existsSync(configToml)) {
1349
- atomicWrite(configToml, [
1350
- "# Created by qualia-framework install.",
1351
- "# User settings can be added normally; Qualia runtime lives in AGENTS.md, hooks.json, agents/, and bin/.",
1352
- "",
1353
- "[features]",
1354
- "hooks = true",
1355
- "plugin_hooks = true",
1356
- "",
1357
- "# Codex's built-in status line is rendered at the bottom of the TUI.",
1358
- "# It takes an ARRAY of pre-defined segment names; Codex does NOT support",
1359
- "# custom-command status lines (unlike Claude's settings.json statusLine),",
1360
- "# so the Qualia phase/state info is rendered via the SessionStart banner",
1361
- "# at the top of the session instead. The segment list below mirrors the",
1362
- "# Codex default rich layout.",
1363
- "[tui]",
1364
- 'status_line = ["model-with-reasoning", "task-progress", "current-dir", "git-branch", "context-used", "five-hour-limit", "weekly-limit"]',
1365
- "status_line_use_colors = true",
1366
- "",
1367
- ].join("\n"));
1339
+ atomicWrite(configToml, next);
1368
1340
  ok("config.toml (minimal Codex config)");
1341
+ } else if (next !== existing) {
1342
+ atomicWrite(configToml, next);
1343
+ ok("config.toml (Codex bottom status line)");
1369
1344
  } else {
1370
- // Existing user config append [tui] block only if absent. Leaves
1371
- // every other user setting untouched.
1372
- try {
1373
- const existing = fs.readFileSync(configToml, "utf8");
1374
- if (!/^\[tui\]/m.test(existing) && !/^status_line\s*=/m.test(existing)) {
1375
- const append = [
1376
- "",
1377
- "# Added by qualia-framework — Codex bottom status line.",
1378
- "[tui]",
1379
- 'status_line = ["model-with-reasoning", "task-progress", "current-dir", "git-branch", "context-used", "five-hour-limit", "weekly-limit"]',
1380
- "status_line_use_colors = true",
1381
- "",
1382
- ].join("\n");
1383
- fs.appendFileSync(configToml, append);
1384
- ok("config.toml (appended [tui] status line block)");
1385
- } else {
1386
- log(`${DIM}config.toml (kept — user has customized)${RESET}`);
1387
- }
1388
- } catch {
1389
- log(`${DIM}config.toml (kept — user has customized)${RESET}`);
1390
- }
1345
+ log(`${DIM}config.toml (keptCodex status line already wired)${RESET}`);
1391
1346
  }
1392
1347
  } catch (e) {
1393
1348
  warn(`Codex config.toml — ${e.message}`);
@@ -1436,20 +1391,7 @@ async function installCodex(member, target) {
1436
1391
  try {
1437
1392
  const binDest = path.join(CODEX_DIR, "bin");
1438
1393
  if (!fs.existsSync(binDest)) fs.mkdirSync(binDest, { recursive: true });
1439
- const scripts = [
1440
- "state.js",
1441
- "qualia-ui.js",
1442
- "statusline.js",
1443
- "knowledge.js",
1444
- "knowledge-flush.js",
1445
- "plan-contract.js",
1446
- "agent-runs.js",
1447
- "slop-detect.mjs",
1448
- "erp-retry.js",
1449
- "report-payload.js",
1450
- "project-snapshot.js",
1451
- "codex-goal.js",
1452
- ];
1394
+ const scripts = binFiles();
1453
1395
  for (const script of scripts) {
1454
1396
  const src = path.join(FRAMEWORK_DIR, "bin", script);
1455
1397
  const out = path.join(binDest, script);
@@ -1487,9 +1429,9 @@ async function installCodex(member, target) {
1487
1429
  const rulesDest = path.join(CODEX_DIR, "rules");
1488
1430
  if (!fs.existsSync(rulesDest)) fs.mkdirSync(rulesDest, { recursive: true });
1489
1431
  for (const file of fs.readdirSync(rulesDir)) {
1490
- copy(path.join(rulesDir, file), path.join(rulesDest, file));
1432
+ copyTextTransform(path.join(rulesDir, file), path.join(rulesDest, file), codexText);
1491
1433
  }
1492
- copyTree(path.join(FRAMEWORK_DIR, "qualia-design"), path.join(CODEX_DIR, "qualia-design"));
1434
+ copyTreeTransform(path.join(FRAMEWORK_DIR, "qualia-design"), path.join(CODEX_DIR, "qualia-design"), codexText);
1493
1435
  ok("rules/ + qualia-design/");
1494
1436
  } catch (e) {
1495
1437
  warn(`Codex rules/design — ${e.message}`);
@@ -1503,9 +1445,9 @@ async function installCodex(member, target) {
1503
1445
  const skillsDest = path.join(CODEX_DIR, "skills");
1504
1446
  const codexPruned = pruneDeprecatedSkills(CODEX_DIR);
1505
1447
  for (const name of codexPruned) ok(`pruned deprecated: ${name}`);
1506
- for (const skill of fs.readdirSync(skillsSrc)) {
1448
+ for (const skill of ACTIVE_SKILLS) {
1507
1449
  const src = path.join(skillsSrc, skill);
1508
- if (!fs.statSync(src).isDirectory()) continue;
1450
+ if (!fs.existsSync(src) || !fs.statSync(src).isDirectory()) continue;
1509
1451
  copyTreeTransform(src, path.join(skillsDest, skill), codexText);
1510
1452
  }
1511
1453
  ok("skills/");
@@ -1582,6 +1524,7 @@ async function installCodex(member, target) {
1582
1524
  hooks: [
1583
1525
  { type: "command", command: nodeCmd("auto-update.js"), timeout: 5, statusMessage: "Qualia update check..." },
1584
1526
  { type: "command", command: nodeCmd("git-guardrails.js"), timeout: 5, statusMessage: "Qualia git safety..." },
1527
+ { type: "command", command: nodeCmd("fawzi-approval-guard.js"), timeout: 5 },
1585
1528
  { type: "command", command: nodeCmd("branch-guard.js"), timeout: 5 },
1586
1529
  { type: "command", command: nodeCmd("pre-push.js"), timeout: 15 },
1587
1530
  { type: "command", command: nodeCmd("pre-deploy-gate.js"), timeout: 180 },
@@ -1593,6 +1536,7 @@ async function installCodex(member, target) {
1593
1536
  {
1594
1537
  matcher: "Edit|Write",
1595
1538
  hooks: [
1539
+ { type: "command", command: nodeCmd("fawzi-approval-guard.js"), timeout: 5 },
1596
1540
  { type: "command", command: nodeCmd("migration-guard.js"), timeout: 10 },
1597
1541
  ],
1598
1542
  },
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  // ~/.claude/bin/knowledge-flush.js — non-interactive memory-layer flush.
3
3
  //
4
- // Wraps `/qualia-flush` so it can run from cron (or systemd timer, or any
5
- // CI/scheduled job) without an interactive Claude Code session. Closes the
4
+ // Runs the Qualia memory flush prompt from cron (or systemd timer, or any
5
+ // CI/scheduled job) without installing a separate slash command. Closes the
6
6
  // memory loop end-to-end:
7
7
  //
8
8
  // Stop hook (auto, every turn) → <install-home>/knowledge/daily-log/{date}.md
9
9
  // THIS SCRIPT (weekly cron) → spawns the installed agent CLI
10
- // /qualia-flush → promotes raw → curated tier
10
+ // flush prompt → promotes raw → curated tier
11
11
  // bin/knowledge.js (every spawn) → reads index.md → reaches the right file
12
12
  //
13
13
  // Usage:
@@ -86,10 +86,9 @@ function which(cmd) {
86
86
  return null;
87
87
  }
88
88
 
89
- // Pass-through args (so `--days 14`, `--dry-run`, `--project X` all reach the
90
- // skill). We don't parse them ourselves the skill is the source of truth
91
- // for argument semantics. We only use `--days` locally to short-circuit when
92
- // the daily-log is genuinely empty.
89
+ // Pass-through args (so `--days 14`, `--dry-run`, `--project X` are visible to
90
+ // the agent prompt). We only parse `--days` locally to short-circuit when the
91
+ // daily-log is genuinely empty.
93
92
  const argv = process.argv.slice(2);
94
93
  const flagIdx = argv.indexOf("--days");
95
94
  const days = flagIdx >= 0 ? parseInt(argv[flagIdx + 1], 10) || 7 : 7;
@@ -128,9 +127,21 @@ if (!dailyLogHasRecentEntries(days)) {
128
127
 
129
128
  // ── Run ──────────────────────────────────────────────────
130
129
  // `claude -p "<prompt>"` and `codex exec "<prompt>"` run a single
131
- // non-interactive turn. The skill body invocation matches what the user would
132
- // type at the prompt.
133
- const prompt = `/qualia-flush ${argv.join(" ")}`.trim();
130
+ // non-interactive turn. Keep the prompt self-contained so no separate flush
131
+ // slash command needs to be installed.
132
+ const argsText = argv.join(" ").trim() || "(none)";
133
+ const prompt = [
134
+ "Run the Qualia memory flush.",
135
+ "",
136
+ `Arguments: ${argsText}`,
137
+ `Install home: ${QUALIA_HOME}`,
138
+ "",
139
+ "Read recent markdown files from knowledge/daily-log under the install home.",
140
+ "Promote recurring patterns, decisions, fixes, and client preferences into the curated knowledge tier using bin/knowledge.js append.",
141
+ "Do not promote one-off noise. If --dry-run is present, report planned promotions without writing.",
142
+ "If --project NAME is present, limit promotions to that project.",
143
+ "Finish with one line starting exactly: ⬢ Flushed daily-log",
144
+ ].join("\n");
134
145
 
135
146
  const cliArgs = IS_CODEX_INSTALL ? ["exec", prompt] : ["-p", prompt];
136
147
  const result = spawnSync(agentBin, cliArgs, {
package/bin/knowledge.js CHANGED
@@ -79,7 +79,7 @@ function readSafe(p) {
79
79
  // 3. Path with "/" → treat as relative to knowledge dir (concepts/foo)
80
80
  // 4. Bare name → look in top-level first; if missing, search known
81
81
  // subdirectories (concepts/, daily-log/) for an exact match. This
82
- // means /qualia-flush can write to concepts/voice-agent-call-state.md
82
+ // means the memory flush can write to concepts/voice-agent-call-state.md
83
83
  // and skills can later run `knowledge.js load voice-agent-call-state`
84
84
  // without knowing it lives in a subdirectory.
85
85
  function resolveFile(name) {