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
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
2
  # Qualia Framework — surface-drift guard
3
3
  # Greps every active surface for backtick-quoted /qualia-{name} command references.
4
- # Asserts each name has a matching skills/qualia-{name}/SKILL.md.
4
+ # Asserts each name is in bin/command-surface.js and has a matching SKILL.md.
5
5
  #
6
6
  # Why: v5.7 + v5.8 removed /qualia-quick, /qualia-task, /qualia-prd, /qualia-design,
7
7
  # /qualia-polish-loop. Three user-facing files (rules/speed.md, templates/help.html,
@@ -81,6 +81,11 @@ SCAN_FILES=$(
81
81
  # We capture name + file:line so we can show context per failure.
82
82
  declare -A SEEN_REFS
83
83
  declare -A REF_LOCATIONS
84
+ declare -A ACTIVE_SKILL_MAP
85
+
86
+ while IFS= read -r skill; do
87
+ [ -n "$skill" ] && ACTIVE_SKILL_MAP["/$skill"]=1
88
+ done < <("$NODE" -e 'const {ACTIVE_SKILLS}=require(process.argv[1]); for (const s of ACTIVE_SKILLS) console.log(s)' "$FRAMEWORK_ROOT/bin/command-surface.js")
84
89
 
85
90
  while IFS= read -r file; do
86
91
  # Pattern A: backtick-quoted commands. Allow trailing flag/word but only capture base name.
@@ -124,11 +129,11 @@ for ref in $(printf '%s\n' "${!SEEN_REFS[@]}" | sort); do
124
129
  name="${ref#/}"
125
130
  skill_dir="$SKILLS_DIR/$name"
126
131
  locations="${REF_LOCATIONS[$ref]}"
127
- if [ -d "$skill_dir" ] && [ -f "$skill_dir/SKILL.md" ]; then
128
- pass "$ref → skills/$name/SKILL.md"
132
+ if [ "${ACTIVE_SKILL_MAP[$ref]:-}" = "1" ] && [ -d "$skill_dir" ] && [ -f "$skill_dir/SKILL.md" ]; then
133
+ pass "$ref → active skills/$name/SKILL.md"
129
134
  continue
130
135
  fi
131
- fail_case "$ref" "no skills/$name/SKILL.md — referenced by: $locations"
136
+ fail_case "$ref" "not an active shipped command or missing skills/$name/SKILL.md — referenced by: $locations"
132
137
  done
133
138
 
134
139
  PACKAGE_VERSION="$("$NODE" -e 'console.log(require(process.argv[1]).version)' "$FRAMEWORK_ROOT/package.json" 2>/dev/null || echo "")"
package/tests/runner.js CHANGED
@@ -212,7 +212,7 @@ describe("CLI", () => {
212
212
  it("team list works", () => {
213
213
  const r = run("cli.js", ["team", "list"]);
214
214
  assert.equal(r.status, 0);
215
- assert.match(stripAnsi(r.stdout), /QS-FAWZI-01/);
215
+ assert.match(stripAnsi(r.stdout), /QS-FAWZI-11/);
216
216
  });
217
217
 
218
218
  it("traces handles missing traces dir", () => {
@@ -230,7 +230,7 @@ describe("CLI", () => {
230
230
  try {
231
231
  fs.mkdirSync(path.join(tmpHome, ".claude"), { recursive: true });
232
232
  fs.writeFileSync(path.join(tmpHome, ".claude", ".qualia-config.json"), JSON.stringify({
233
- code: "QS-FAWZI-01",
233
+ code: "QS-FAWZI-11",
234
234
  installed_by: "Fawzi Goussous",
235
235
  role: "OWNER",
236
236
  version: "2.8.1",
@@ -1607,7 +1607,7 @@ describe("Hooks", () => {
1607
1607
  try {
1608
1608
  fs.mkdirSync(path.join(tmpHome, ".claude"), { recursive: true });
1609
1609
  fs.writeFileSync(path.join(tmpHome, ".claude", ".qualia-config.json"), JSON.stringify({
1610
- code: "QS-FAWZI-01", version: "99.99.99",
1610
+ code: "QS-FAWZI-11", version: "99.99.99",
1611
1611
  }));
1612
1612
  const r = spawnSync(NODE, [path.join(HOOKS, "auto-update.js")], {
1613
1613
  encoding: "utf8", timeout: 5000,
@@ -2441,7 +2441,7 @@ describe("qualia-ui.js", () => {
2441
2441
  try {
2442
2442
  fs.mkdirSync(path.join(tmpHome, ".claude"), { recursive: true });
2443
2443
  fs.writeFileSync(path.join(tmpHome, ".claude", ".qualia-config.json"), JSON.stringify({
2444
- code: "QS-FAWZI-01",
2444
+ code: "QS-FAWZI-11",
2445
2445
  installed_by: "Fawzi Goussous",
2446
2446
  role: "OWNER",
2447
2447
  version: "2.8.1",
@@ -2535,7 +2535,7 @@ describe("install.js", () => {
2535
2535
  it("valid code installs everything", () => {
2536
2536
  const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-install-"));
2537
2537
  try {
2538
- const r = runInstall("QS-FAWZI-01", tmpHome);
2538
+ const r = runInstall("QS-FAWZI-11", tmpHome);
2539
2539
  assert.equal(r.status, 0);
2540
2540
  assert.ok(fs.existsSync(path.join(tmpHome, ".claude", "skills", "qualia", "SKILL.md")));
2541
2541
  assert.ok(fs.existsSync(path.join(tmpHome, ".claude", "hooks", "session-start.js")));
@@ -2556,9 +2556,9 @@ describe("install.js", () => {
2556
2556
  it("config JSON has correct fields", () => {
2557
2557
  const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-install-"));
2558
2558
  try {
2559
- runInstall("QS-FAWZI-01", tmpHome);
2559
+ runInstall("QS-FAWZI-11", tmpHome);
2560
2560
  const config = JSON.parse(fs.readFileSync(path.join(tmpHome, ".claude", ".qualia-config.json"), "utf8"));
2561
- assert.equal(config.code, "QS-FAWZI-01");
2561
+ assert.equal(config.code, "QS-FAWZI-11");
2562
2562
  assert.equal(config.installed_by, "Fawzi Goussous");
2563
2563
  assert.equal(config.role, "OWNER");
2564
2564
  } finally {
@@ -2569,7 +2569,7 @@ describe("install.js", () => {
2569
2569
  it("CLAUDE.md role placeholder replaced", () => {
2570
2570
  const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-install-"));
2571
2571
  try {
2572
- runInstall("QS-FAWZI-01", tmpHome);
2572
+ runInstall("QS-FAWZI-11", tmpHome);
2573
2573
  const claude = fs.readFileSync(path.join(tmpHome, ".claude", "CLAUDE.md"), "utf8");
2574
2574
  assert.match(claude, /Role: OWNER/);
2575
2575
  assert.doesNotMatch(claude, /\{\{ROLE\}\}/);
@@ -2578,12 +2578,13 @@ describe("install.js", () => {
2578
2578
  }
2579
2579
  });
2580
2580
 
2581
- it("11 hooks installed (v6.2.0: pre-compact removed; v5.0: vercel-account-guard, env-empty-guard, supabase-destructive-guard added)", () => {
2581
+ it("12 hooks installed (v6.2.11: proxy approval guard added; v6.2.0: pre-compact removed)", () => {
2582
2582
  const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-install-"));
2583
2583
  try {
2584
- runInstall("QS-FAWZI-01", tmpHome);
2584
+ runInstall("QS-FAWZI-11", tmpHome);
2585
2585
  const hooks = fs.readdirSync(path.join(tmpHome, ".claude", "hooks")).filter(f => f.endsWith(".js"));
2586
- assert.equal(hooks.length, 11, `expected 11 hooks, got ${hooks.length}: ${hooks.join(", ")}`);
2586
+ assert.equal(hooks.length, 12, `expected 12 hooks, got ${hooks.length}: ${hooks.join(", ")}`);
2587
+ assert.ok(hooks.includes("fawzi-approval-guard.js"), "fawzi-approval-guard.js must be installed");
2587
2588
  assert.ok(!hooks.includes("pre-compact.js"), "pre-compact.js must NOT be installed (removed in v6.2.0)");
2588
2589
  } finally {
2589
2590
  fs.rmSync(tmpHome, { recursive: true, force: true });
@@ -2593,7 +2594,7 @@ describe("install.js", () => {
2593
2594
  it("settings.json has hooks and statusLine", () => {
2594
2595
  const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-install-"));
2595
2596
  try {
2596
- runInstall("QS-FAWZI-01", tmpHome);
2597
+ runInstall("QS-FAWZI-11", tmpHome);
2597
2598
  const settings = fs.readFileSync(path.join(tmpHome, ".claude", "settings.json"), "utf8");
2598
2599
  assert.match(settings, /SessionStart/);
2599
2600
  assert.match(settings, /PreToolUse/);
@@ -2606,10 +2607,10 @@ describe("install.js", () => {
2606
2607
  it("lowercase code is normalized", () => {
2607
2608
  const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-install-"));
2608
2609
  try {
2609
- const r = runInstall("qs-fawzi-01", tmpHome);
2610
+ const r = runInstall("qs-fawzi-11", tmpHome);
2610
2611
  assert.equal(r.status, 0);
2611
2612
  const config = JSON.parse(fs.readFileSync(path.join(tmpHome, ".claude", ".qualia-config.json"), "utf8"));
2612
- assert.equal(config.code, "QS-FAWZI-01");
2613
+ assert.equal(config.code, "QS-FAWZI-11");
2613
2614
  } finally {
2614
2615
  fs.rmSync(tmpHome, { recursive: true, force: true });
2615
2616
  }
@@ -2618,10 +2619,10 @@ describe("install.js", () => {
2618
2619
  it("O/0 typo tolerance in code suffix", () => {
2619
2620
  const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-install-"));
2620
2621
  try {
2621
- const r = runInstall("QS-FAWZI-O1", tmpHome);
2622
+ const r = runInstall("QS-HASAN-O2", tmpHome);
2622
2623
  assert.equal(r.status, 0);
2623
2624
  const config = JSON.parse(fs.readFileSync(path.join(tmpHome, ".claude", ".qualia-config.json"), "utf8"));
2624
- assert.equal(config.code, "QS-FAWZI-01");
2625
+ assert.equal(config.code, "QS-HASAN-02");
2625
2626
  } finally {
2626
2627
  fs.rmSync(tmpHome, { recursive: true, force: true });
2627
2628
  }
@@ -2672,10 +2673,10 @@ describe("install.js", () => {
2672
2673
  it("whitespace-padded code is accepted", () => {
2673
2674
  const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-install-"));
2674
2675
  try {
2675
- const r = runInstall(" QS-FAWZI-01 ", tmpHome);
2676
+ const r = runInstall(" QS-FAWZI-11 ", tmpHome);
2676
2677
  assert.equal(r.status, 0);
2677
2678
  const config = JSON.parse(fs.readFileSync(path.join(tmpHome, ".claude", ".qualia-config.json"), "utf8"));
2678
- assert.equal(config.code, "QS-FAWZI-01");
2679
+ assert.equal(config.code, "QS-FAWZI-11");
2679
2680
  } finally {
2680
2681
  fs.rmSync(tmpHome, { recursive: true, force: true });
2681
2682
  }
@@ -2689,7 +2690,7 @@ describe("install.js", () => {
2689
2690
  customKey: "preserved",
2690
2691
  env: { MY_CUSTOM_VAR: "hello" },
2691
2692
  }));
2692
- const r = runInstall("QS-FAWZI-01", tmpHome);
2693
+ const r = runInstall("QS-FAWZI-11", tmpHome);
2693
2694
  assert.equal(r.status, 0);
2694
2695
  const settings = JSON.parse(fs.readFileSync(path.join(tmpHome, ".claude", "settings.json"), "utf8"));
2695
2696
  assert.equal(settings.customKey, "preserved");
@@ -2704,7 +2705,7 @@ describe("install.js", () => {
2704
2705
  it("knowledge files created on first install", () => {
2705
2706
  const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-install-"));
2706
2707
  try {
2707
- runInstall("QS-FAWZI-01", tmpHome);
2708
+ runInstall("QS-FAWZI-11", tmpHome);
2708
2709
  assert.ok(fs.existsSync(path.join(tmpHome, ".claude", "knowledge", "learned-patterns.md")));
2709
2710
  assert.ok(fs.existsSync(path.join(tmpHome, ".claude", "knowledge", "common-fixes.md")));
2710
2711
  assert.ok(fs.existsSync(path.join(tmpHome, ".claude", "knowledge", "client-prefs.md")));
@@ -2716,10 +2717,10 @@ describe("install.js", () => {
2716
2717
  it("re-install preserves user edits in knowledge files", () => {
2717
2718
  const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-install-"));
2718
2719
  try {
2719
- runInstall("QS-FAWZI-01", tmpHome);
2720
+ runInstall("QS-FAWZI-11", tmpHome);
2720
2721
  fs.appendFileSync(path.join(tmpHome, ".claude", "knowledge", "learned-patterns.md"),
2721
2722
  "\n## CUSTOM LEARNING — DO NOT OVERWRITE\n");
2722
- runInstall("QS-FAWZI-01", tmpHome);
2723
+ runInstall("QS-FAWZI-11", tmpHome);
2723
2724
  const content = fs.readFileSync(path.join(tmpHome, ".claude", "knowledge", "learned-patterns.md"), "utf8");
2724
2725
  assert.match(content, /CUSTOM LEARNING/);
2725
2726
  } finally {
@@ -2732,7 +2733,7 @@ describe("install.js", () => {
2732
2733
  const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-install-"));
2733
2734
  try {
2734
2735
  // Fresh install first, then inject a user-owned hook, then reinstall.
2735
- runInstall("QS-FAWZI-01", tmpHome);
2736
+ runInstall("QS-FAWZI-11", tmpHome);
2736
2737
  const settingsPath = path.join(tmpHome, ".claude", "settings.json");
2737
2738
  const before = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
2738
2739
 
@@ -2746,7 +2747,7 @@ describe("install.js", () => {
2746
2747
  before.hooks.PreToolUse = [userHook, ...(before.hooks.PreToolUse || [])];
2747
2748
  fs.writeFileSync(settingsPath, JSON.stringify(before, null, 2));
2748
2749
 
2749
- const r = runInstall("QS-FAWZI-01", tmpHome);
2750
+ const r = runInstall("QS-FAWZI-11", tmpHome);
2750
2751
  assert.equal(r.status, 0);
2751
2752
  const after = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
2752
2753
  const allCmds = [];
@@ -2770,7 +2771,7 @@ describe("install.js", () => {
2770
2771
  it("templates copied to qualia-templates/", () => {
2771
2772
  const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-install-"));
2772
2773
  try {
2773
- runInstall("QS-FAWZI-01", tmpHome);
2774
+ runInstall("QS-FAWZI-11", tmpHome);
2774
2775
  const tmplDir = path.join(tmpHome, ".claude", "qualia-templates");
2775
2776
  assert.ok(fs.existsSync(tmplDir));
2776
2777
  const files = fs.readdirSync(tmplDir);
@@ -2783,7 +2784,7 @@ describe("install.js", () => {
2783
2784
  it("agents copied", () => {
2784
2785
  const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-install-"));
2785
2786
  try {
2786
- runInstall("QS-FAWZI-01", tmpHome);
2787
+ runInstall("QS-FAWZI-11", tmpHome);
2787
2788
  const agentDir = path.join(tmpHome, ".claude", "agents");
2788
2789
  assert.ok(fs.existsSync(agentDir));
2789
2790
  const files = fs.readdirSync(agentDir);
@@ -2796,11 +2797,14 @@ describe("install.js", () => {
2796
2797
  it("rules copied", () => {
2797
2798
  const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-install-"));
2798
2799
  try {
2799
- runInstall("QS-FAWZI-01", tmpHome);
2800
+ runInstall("QS-FAWZI-11", tmpHome);
2800
2801
  const rulesDir = path.join(tmpHome, ".claude", "rules");
2801
2802
  assert.ok(fs.existsSync(rulesDir));
2802
2803
  const files = fs.readdirSync(rulesDir);
2803
2804
  assert.ok(files.length > 0);
2805
+ assert.ok(fs.existsSync(path.join(rulesDir, "command-output.md")));
2806
+ assert.match(fs.readFileSync(path.join(rulesDir, "command-output.md"), "utf8"), /alwaysApply: true/);
2807
+ assert.ok(fs.existsSync(path.join(tmpHome, ".claude", "qualia-design", "graphics.md")));
2804
2808
  } finally {
2805
2809
  fs.rmSync(tmpHome, { recursive: true, force: true });
2806
2810
  }
@@ -2809,7 +2813,7 @@ describe("install.js", () => {
2809
2813
  it("config version matches package.json", () => {
2810
2814
  const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-install-"));
2811
2815
  try {
2812
- runInstall("QS-FAWZI-01", tmpHome);
2816
+ runInstall("QS-FAWZI-11", tmpHome);
2813
2817
  const config = JSON.parse(fs.readFileSync(path.join(tmpHome, ".claude", ".qualia-config.json"), "utf8"));
2814
2818
  assert.equal(config.version, PKG_VERSION);
2815
2819
  } finally {
@@ -106,7 +106,7 @@ for skill_dir in "$SKILLS_DIR"/*/; do
106
106
 
107
107
  # Cache-aware spawn audit (per rules/grounding.md):
108
108
  # Every spawn to a CUSTOM (qualia-*) agent must anchor the prompt with
109
- # `@~/.claude/agents/{name}.md` (either `Role: @...` or `Read your role:
109
+ # `@${QUALIA_AGENTS}/{name}.md` (either `Role: @...` or `Read your role:
110
110
  # @...` — both forms accepted). The role file is session-stable; placing
111
111
  # it first lets Anthropic's prompt cache reuse the prefix across spawns
112
112
  # (documented 81-90% cost reduction). If task-specific content lands
@@ -125,14 +125,14 @@ for skill_dir in "$SKILLS_DIR"/*/; do
125
125
  custom_spawn_count=$((custom_spawn_count + $(grep -c 'subagent_type="qualia-' "$ref_md")))
126
126
  fi
127
127
  if [ "${custom_spawn_count:-0}" -gt 0 ]; then
128
- role_count=$(grep -cE '@~/\.claude/agents/' "$skill_md")
128
+ role_count=$(grep -cF '@${QUALIA_AGENTS}/' "$skill_md")
129
129
  if [ -f "$ref_md" ]; then
130
- role_count=$((role_count + $(grep -cE '@~/\.claude/agents/' "$ref_md")))
130
+ role_count=$((role_count + $(grep -cF '@${QUALIA_AGENTS}/' "$ref_md")))
131
131
  fi
132
132
  if [ "${role_count:-0}" -ge "$custom_spawn_count" ]; then
133
133
  pass "$name: spawn audit ($custom_spawn_count custom spawn(s), all role-anchored for cache)"
134
134
  else
135
- fail_case "$name: spawn audit" "$custom_spawn_count custom spawn(s) but only ${role_count:-0} '@~/.claude/agents/' anchors — prompt cache will miss"
135
+ fail_case "$name: spawn audit" "$custom_spawn_count custom spawn(s) but only ${role_count:-0} '@\${QUALIA_AGENTS}/' anchors — prompt cache will miss"
136
136
  fi
137
137
  fi
138
138
  done
@@ -6,6 +6,8 @@ PASS=0
6
6
  FAIL=0
7
7
  # Resolve STATE_JS to an ABSOLUTE path so `cd` inside subshells doesn't break it.
8
8
  STATE_JS="$(cd "$(dirname "$0")/../bin" && pwd)/state.js"
9
+ FRAMEWORK_DIR="$(cd "$(dirname "$0")/.." && pwd)"
10
+ STATE_LEDGER_JS="$FRAMEWORK_DIR/bin/state-ledger.js"
9
11
  NODE="${NODE:-node}"
10
12
 
11
13
  # Track tmp dirs we create so we can clean them up on exit
@@ -73,6 +75,47 @@ Goal: Test goal
73
75
  PLAN
74
76
  }
75
77
 
78
+ make_valid_contract() {
79
+ local dir="$1"
80
+ local phase="${2:-1}"
81
+ cat > "$dir/.planning/phase-${phase}-contract.json" <<'JSON'
82
+ {
83
+ "version": 1,
84
+ "phase": 1,
85
+ "goal": "Test goal",
86
+ "why": "Exercise machine evidence",
87
+ "generated_at": "2026-05-23T00:00:00.000Z",
88
+ "generated_by": "manual",
89
+ "source_plan_hash": "",
90
+ "success_criteria": ["Machine check passes"],
91
+ "tasks": [
92
+ {
93
+ "id": "T1",
94
+ "title": "Machine check",
95
+ "wave": 1,
96
+ "depends_on": [],
97
+ "persona": "none",
98
+ "files_modify": [],
99
+ "files_create": [],
100
+ "files_delete": [],
101
+ "acceptance_criteria": ["Node command exits 0"],
102
+ "action": "Run deterministic evidence check",
103
+ "context_files": [],
104
+ "verification": [
105
+ {
106
+ "type": "command-exit",
107
+ "command": "node",
108
+ "args": ["-e", "process.exit(0)"],
109
+ "expected_exit": 0,
110
+ "timeout_ms": 5000
111
+ }
112
+ ]
113
+ }
114
+ ]
115
+ }
116
+ JSON
117
+ }
118
+
76
119
  echo "=== state.js Behavioral Tests ==="
77
120
  echo ""
78
121
 
@@ -98,11 +141,18 @@ INIT_EXIT=$?
98
141
  if [ "$INIT_EXIT" -eq 0 ] \
99
142
  && [ -f "$TMP/.planning/tracking.json" ] \
100
143
  && [ -f "$TMP/.planning/STATE.md" ] \
144
+ && [ -f "$TMP/.planning/qualia/state.jsonl" ] \
101
145
  && grep -q '"ok": true' /tmp/qualia-state-test.out \
102
146
  && grep -q '"action": "init"' /tmp/qualia-state-test.out; then
103
- pass "cmdInit creates tracking.json + STATE.md"
147
+ pass "cmdInit creates tracking.json + STATE.md + ledger"
104
148
  else
105
- fail_case "cmdInit creates tracking.json + STATE.md" "exit=$INIT_EXIT"
149
+ fail_case "cmdInit creates tracking.json + STATE.md + ledger" "exit=$INIT_EXIT"
150
+ fi
151
+
152
+ if (cd "$TMP" && $NODE "$STATE_LEDGER_JS" validate 2>&1 | grep -q '"ok": true'); then
153
+ pass "cmdInit writes valid state ledger"
154
+ else
155
+ fail_case "cmdInit state ledger validates"
106
156
  fi
107
157
 
108
158
  # tracking.json content sanity
@@ -175,13 +225,22 @@ EXIT=$?
175
225
  if [ "$EXIT" -eq 0 ] \
176
226
  && echo "$OUT" | grep -q '"ok": true' \
177
227
  && echo "$OUT" | grep -q '"status": "built"' \
228
+ && echo "$OUT" | grep -q '"ledger_event_id":' \
178
229
  && grep -q '"tasks_done": 5' "$TMP/.planning/tracking.json" \
179
230
  && grep -q '"tasks_total": 5' "$TMP/.planning/tracking.json"; then
180
- pass "planned → built records tasks_done/tasks_total"
231
+ pass "planned → built records tasks_done/tasks_total + ledger id"
181
232
  else
182
233
  fail_case "planned → built" "exit=$EXIT"
183
234
  fi
184
235
 
236
+ LEDGER_COUNT=$(wc -l < "$TMP/.planning/qualia/state.jsonl" | tr -d ' ')
237
+ if [ "$LEDGER_COUNT" -ge 3 ] \
238
+ && (cd "$TMP" && $NODE "$STATE_LEDGER_JS" validate 2>&1 | grep -q '"ok": true'); then
239
+ pass "state ledger records init + transitions as valid chain"
240
+ else
241
+ fail_case "state ledger transition chain" "count=$LEDGER_COUNT"
242
+ fi
243
+
185
244
  # 6. built → verified(pass) auto-advances to phase 2, resets status to setup
186
245
  touch "$TMP/.planning/phase-1-verification.md"
187
246
  OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass 2>&1)
@@ -195,6 +254,33 @@ else
195
254
  fail_case "built → verified(pass) auto-advance" "exit=$EXIT out=$OUT"
196
255
  fi
197
256
 
257
+ # 6b. verified(pass) refuses when a contract exists but machine evidence is missing
258
+ TMP=$(make_project)
259
+ make_valid_plan "$TMP" 1
260
+ make_valid_contract "$TMP" 1
261
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
262
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
263
+ echo "result: PASS" > "$TMP/.planning/phase-1-verification.md"
264
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass 2>&1)
265
+ EXIT=$?
266
+ if [ "$EXIT" -ne 0 ] && echo "$OUT" | grep -q '"error": "MISSING_EVIDENCE"'; then
267
+ pass "verified(pass) refuses missing machine evidence when contract exists"
268
+ else
269
+ fail_case "verified(pass) missing evidence guard" "exit=$EXIT out=$OUT"
270
+ fi
271
+
272
+ # 6c. verified(pass) succeeds after contract-runner writes passing evidence
273
+ (cd "$TMP" && $NODE "$FRAMEWORK_DIR/bin/contract-runner.js" .planning/phase-1-contract.json >/dev/null 2>&1)
274
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass 2>&1)
275
+ EXIT=$?
276
+ if [ "$EXIT" -eq 0 ] \
277
+ && echo "$OUT" | grep -q '"ok": true' \
278
+ && echo "$OUT" | grep -q '"phase": 2'; then
279
+ pass "verified(pass) accepts passing machine evidence"
280
+ else
281
+ fail_case "verified(pass) with machine evidence" "exit=$EXIT out=$OUT"
282
+ fi
283
+
198
284
  # 7. built → verified(fail) stays on phase 1, records verification=fail
199
285
  TMP=$(make_project)
200
286
  make_valid_plan "$TMP" 1
@@ -618,6 +704,50 @@ else
618
704
  fail_case "validate well-formed plan" "exit=$EXIT out=$OUT"
619
705
  fi
620
706
 
707
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" validate-plan --phase 1 --require-contract 2>&1)
708
+ EXIT=$?
709
+ if [ "$EXIT" -eq 1 ] \
710
+ && echo "$OUT" | grep -q 'JSON contract missing'; then
711
+ pass "validate-plan --require-contract fails when JSON contract is missing"
712
+ else
713
+ fail_case "validate-plan --require-contract missing contract" "exit=$EXIT out=$OUT"
714
+ fi
715
+
716
+ HASH=$(cd "$TMP" && $NODE -e "const pc=require('$FRAMEWORK_DIR/bin/plan-contract.js'); const fs=require('fs'); process.stdout.write(pc.hashPlan(fs.readFileSync('.planning/phase-1-plan.md','utf8')))")
717
+ cat > "$TMP/.planning/phase-1-contract.json" <<JSON
718
+ {
719
+ "version": 1,
720
+ "phase": 1,
721
+ "goal": "test",
722
+ "why": "validate require-contract",
723
+ "generated_at": "2026-05-23T00:00:00Z",
724
+ "generated_by": "manual",
725
+ "source_plan_hash": "$HASH",
726
+ "success_criteria": ["ok"],
727
+ "tasks": [{
728
+ "id": "T1",
729
+ "title": "Task",
730
+ "wave": 1,
731
+ "depends_on": [],
732
+ "files_modify": [],
733
+ "files_create": [],
734
+ "files_delete": [],
735
+ "acceptance_criteria": ["ok"],
736
+ "action": "Do task",
737
+ "context_files": [],
738
+ "verification": [{ "type": "file-exists", "path": ".planning/phase-1-plan.md" }]
739
+ }]
740
+ }
741
+ JSON
742
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" validate-plan --phase 1 --require-contract 2>&1)
743
+ EXIT=$?
744
+ if [ "$EXIT" -eq 0 ] \
745
+ && echo "$OUT" | grep -q '"contract_status": "valid"'; then
746
+ pass "validate-plan --require-contract accepts valid JSON contract"
747
+ else
748
+ fail_case "validate-plan --require-contract valid contract" "exit=$EXIT out=$OUT"
749
+ fi
750
+
621
751
  # 33. validate-plan rejects empty plan
622
752
  TMP=$(make_project)
623
753
  echo "" > "$TMP/.planning/phase-1-plan.md"
@@ -1,185 +0,0 @@
1
- ---
2
- name: qualia-debug
3
- description: "Investigative debugging — parses symptom from arguments, runs diagnostic scans, identifies root cause, applies minimal fix, writes DEBUG report. Trigger on 'debug', 'find bug', 'fix error', 'something is broken', 'not working', 'weird behavior', 'layout broken', 'CSS issue', 'slow page', 'performance'."
4
- allowed-tools:
5
- - Bash
6
- - Read
7
- - Edit
8
- - Write
9
- - Grep
10
- - Glob
11
- - Agent
12
- ---
13
-
14
- # /qualia-debug — Investigative Debugging (one-shot)
15
-
16
- Parse the symptom. Run diagnostics. Find root cause. Apply minimal fix. Write report. **One-shot — no mandatory user questions.**
17
-
18
- ## Usage
19
-
20
- - `/qualia-debug {symptom}` — investigate a specific symptom
21
- - `/qualia-debug` — no symptom given: investigate recently-changed files for obvious bugs
22
- - `/qualia-debug --frontend {symptom}` — layout/z-index/overflow bias
23
- - `/qualia-debug --perf {symptom}` — performance bias
24
-
25
- ## Tool Budget
26
-
27
- Max 10 Read/Grep/Bash calls for investigation. If you haven't narrowed to root cause in 10, return `INSUFFICIENT EVIDENCE after 10 steps. Narrowed to: {files}. Recommend: {next diagnostic}.` Do not keep guessing.
28
-
29
- ## Process
30
-
31
- ```bash
32
- node ~/.claude/bin/qualia-ui.js banner debug
33
- ```
34
-
35
- ### 1. Parse Symptom from $ARGUMENTS
36
-
37
- - If arguments provided → that's the symptom. Extract: what's broken, where (file/page/feature), when (on click? on load? after change?).
38
- - If arguments empty → run `git diff HEAD~3 --stat` to find recently-touched files. Treat those as the suspect set. Symptom = "something in recent changes".
39
-
40
- ### 2. Check Known Fixes First (cheap)
41
-
42
- ```bash
43
- node ~/.claude/bin/knowledge.js search "{symptom_keywords}"
44
- ```
45
-
46
- If a known fix matches, apply it and jump to step 5 (verify). Known fixes are pre-verified patterns — no need to re-investigate.
47
-
48
- ### 3. Diagnostic Scan
49
-
50
- Run the scan matching the symptom type. All commands in a scan block run as parallel Bash calls in a single response turn.
51
-
52
- **General mode (default):**
53
- ```bash
54
- # Compile errors
55
- npx tsc --noEmit 2>&1 | grep "error TS" | head -20
56
-
57
- # Empty catch / swallowed errors
58
- grep -rn "catch\s*{}\|catch\s*(.*)\s*{\s*}" --include="*.ts" --include="*.tsx" app/ components/ src/ lib/ 2>/dev/null | head -10
59
-
60
- # Recent console.error or thrown errors
61
- grep -rn "console\.error\|throw new" --include="*.ts" --include="*.tsx" app/ components/ src/ lib/ 2>/dev/null | head -10
62
-
63
- # Broken imports
64
- npx tsc --noEmit 2>&1 | grep -i "Cannot find module\|has no exported"
65
- ```
66
-
67
- **Frontend mode (`--frontend`):**
68
- ```bash
69
- # Stacking context audit (z-index issues)
70
- grep -rn "z-index\|z-\[" --include="*.tsx" --include="*.css" app/ components/ src/ 2>/dev/null | head -20
71
-
72
- # Overflow candidates (horizontal scroll, clipping)
73
- grep -rn "100vw\|overflow.*hidden\|overflow-x\|position.*fixed" --include="*.tsx" --include="*.css" app/ components/ src/ 2>/dev/null | head -15
74
-
75
- # Fixed dimensions breaking mobile
76
- grep -rn "width:.*[0-9]\+px\|height:.*[0-9]\+px\|w-\[[0-9]\+px\|h-\[[0-9]\+px" --include="*.tsx" --include="*.css" app/ components/ src/ 2>/dev/null | grep -v "min-\|max-" | head -10
77
-
78
- # Flex/grid blowout candidates
79
- grep -rn "flex\|grid" --include="*.tsx" app/ components/ src/ 2>/dev/null | grep -v "min-w-0\|minmax(0" | wc -l
80
- ```
81
-
82
- **Perf mode (`--perf`):**
83
- ```bash
84
- # Sequential awaits that should be Promise.all
85
- grep -rn "const.*=.*await" --include="*.tsx" --include="*.ts" app/ src/ 2>/dev/null | grep -v "Promise.all\|Promise.allSettled" | head -15
86
-
87
- # Large files
88
- find app/ components/ src/ -name "*.tsx" -o -name "*.ts" 2>/dev/null | xargs wc -l 2>/dev/null | sort -rn | head -10
89
-
90
- # Missing next/image
91
- grep -rn "<img " --include="*.tsx" --include="*.jsx" app/ components/ src/ 2>/dev/null | grep -v "next/image" | wc -l
92
-
93
- # No dynamic imports (possible big bundles)
94
- grep -rn "import(\|next/dynamic" --include="*.tsx" --include="*.ts" app/ src/ 2>/dev/null | wc -l
95
-
96
- # Client-boundary usage
97
- grep -rln "'use client'" --include="*.tsx" app/ components/ src/ 2>/dev/null | wc -l
98
- ```
99
-
100
- ### 4. Form Hypothesis + Apply Minimal Fix
101
-
102
- From the diagnostic output, state the root cause in one sentence with `file:line` citation. No hedging — either you have evidence or you write `INSUFFICIENT EVIDENCE` and return (step 6).
103
-
104
- Apply the minimal fix:
105
- - Only edit files whose contents caused the symptom
106
- - One concept per commit — don't fold in cleanup
107
- - Don't refactor adjacent code
108
- - If the fix touches > 3 files, stop and ask the user first (major refactor disguised as a debug)
109
-
110
- ### 5. Verify Fix
111
-
112
- ```bash
113
- # TypeScript still compiles?
114
- npx tsc --noEmit 2>&1 | grep -c "error TS" # Expect 0
115
-
116
- # Symptom reproduction — ideally a grep that would have matched the bug
117
- # and now returns empty:
118
- grep -rn "{pattern that represented the bug}" {scope} 2>/dev/null
119
- ```
120
-
121
- If the verification fails, revert and return to step 3 with narrower hypothesis.
122
-
123
- ### 6. Write DEBUG Report
124
-
125
- Write to `.planning/DEBUG-{YYYY-MM-DD-HHMM}.md`:
126
-
127
- ```markdown
128
- # Debug Report — {YYYY-MM-DD HH:MM}
129
-
130
- **Symptom:** {user description or "recent changes" if no args}
131
- **Mode:** general | frontend | perf
132
- **Tool calls used:** {N}/10
133
-
134
- ## Investigation
135
- - Diagnostic scans run: {list}
136
- - Files examined: {list}
137
- - Patterns searched: {list}
138
-
139
- ## Root Cause
140
- {file:line} — "{quoted problematic code}" — {explanation of why it caused the symptom}
141
-
142
- ## Fix Applied
143
- - Files: {list}
144
- - Diff summary: {one paragraph}
145
- - Verification: {commands run + results}
146
-
147
- ## Related Observations
148
- - {any adjacent issues noticed but NOT fixed in this debug pass}
149
- ```
150
-
151
- ### 7. Commit
152
-
153
- ```bash
154
- git add {specific files you changed}
155
- git commit -m "fix: {what was broken and why}"
156
- ```
157
-
158
- ## INSUFFICIENT EVIDENCE Return
159
-
160
- If you exhaust the 10-call budget without a confident root cause:
161
-
162
- ```markdown
163
- # Debug Report — {YYYY-MM-DD HH:MM}
164
-
165
- **Symptom:** {description}
166
- **Outcome:** INSUFFICIENT EVIDENCE after 10 inspection steps
167
-
168
- ## Narrowed To
169
- - Files examined: {list}
170
- - Ruled out: {list}
171
- - Remaining suspects: {list}
172
-
173
- ## Recommended Next Diagnostic
174
- - {specific next step for the user — e.g., "run `npm run dev` and watch browser console for the specific error", or "add console.log at file:line and reproduce"}
175
- ```
176
-
177
- Do NOT apply a speculative fix. Return the report and stop.
178
-
179
- ## Rules
180
-
181
- - **No mandatory questions.** This is one-shot. If symptom args are missing, investigate recent changes.
182
- - **Root cause or INSUFFICIENT EVIDENCE** — no "probably" fixes.
183
- - **Minimal fix only.** One concept, one commit. No refactors dressed as debug.
184
- - **Tool budget is hard.** 10 calls, then stop.
185
- - **Every fix gets a DEBUG report in .planning/** — creates a searchable record.