qualia-framework 7.0.1 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/tests/runner.js CHANGED
@@ -2655,12 +2655,12 @@ describe("install.js", () => {
2655
2655
  }
2656
2656
  });
2657
2657
 
2658
- it("15 hooks installed (v6.13: task-write-guard added — R1 plan-contract file-scope guard)", () => {
2658
+ it("16 hooks installed (secret-guard added — commit-time secret gate)", () => {
2659
2659
  const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-install-"));
2660
2660
  try {
2661
2661
  runInstall("QS-FAWZI-11", tmpHome);
2662
2662
  const hooks = fs.readdirSync(path.join(tmpHome, ".claude", "hooks")).filter(f => f.endsWith(".js"));
2663
- assert.equal(hooks.length, 15, `expected 15 hooks, got ${hooks.length}: ${hooks.join(", ")}`);
2663
+ assert.equal(hooks.length, 16, `expected 16 hooks, got ${hooks.length}: ${hooks.join(", ")}`);
2664
2664
  assert.ok(hooks.includes("fawzi-approval-guard.js"), "fawzi-approval-guard.js must be installed");
2665
2665
  assert.ok(hooks.includes("pre-compact.js"), "pre-compact.js must be installed (v6.3.2 sidecar-snapshot mechanism)");
2666
2666
  assert.ok(hooks.includes("task-write-guard.js"), "task-write-guard.js must be installed (v6.13 R1 guard)");
@@ -0,0 +1,62 @@
1
+ #!/bin/bash
2
+ # Qualia Framework — Claude/Codex hook-registration parity
3
+ #
4
+ # A blocking gate that runs on Claude but not Codex is a silent hole: the
5
+ # framework's gates-over-prompts guarantee only holds if every deterministic
6
+ # guard fires on BOTH supported runtimes. This suite asserts the Codex hook
7
+ # registration in bin/install.js carries the same critical blocking gates as
8
+ # the Claude registration. The regression it guards: task-write-guard.js was
9
+ # registered on Claude (Edit|Write) but omitted on Codex, so off-contract /
10
+ # fabricated-file writes went unblocked on Codex.
11
+ #
12
+ # Run: bash tests/runtime-parity.test.sh
13
+
14
+ PASS=0
15
+ FAIL=0
16
+ INSTALL="$(cd "$(dirname "$0")/.." && pwd)/bin/install.js"
17
+
18
+ pass() { echo " ✓ $1"; PASS=$((PASS + 1)); }
19
+ fail_case() { echo " ✗ $1"; echo " $2"; FAIL=$((FAIL + 1)); }
20
+
21
+ echo "runtime-parity.test.sh — Claude/Codex hook registration parity"
22
+ echo ""
23
+
24
+ if [ ! -f "$INSTALL" ]; then
25
+ fail_case "install.js exists" "$INSTALL not found"
26
+ echo "=== Results: $PASS passed, $FAIL failed ==="
27
+ exit 1
28
+ fi
29
+
30
+ # Extract the Codex hook registration object (const qualiaHooks = { ... };).
31
+ CODEX_BLOCK=$(awk '/const qualiaHooks = \{/{f=1} f{print} f&&/^ \};/{exit}' "$INSTALL")
32
+
33
+ if [ -z "$CODEX_BLOCK" ]; then
34
+ fail_case "locate Codex qualiaHooks block" "could not extract 'const qualiaHooks = { ... }' from install.js"
35
+ echo "=== Results: $PASS passed, $FAIL failed ==="
36
+ exit 1
37
+ fi
38
+ pass "located the Codex qualiaHooks registration block"
39
+
40
+ # Critical blocking gates that MUST be registered on Codex (superset check).
41
+ CRITICAL_GATES="task-write-guard.js branch-guard.js migration-guard.js pre-deploy-gate.js supabase-destructive-guard.js fawzi-approval-guard.js secret-guard.js"
42
+
43
+ for gate in $CRITICAL_GATES; do
44
+ if echo "$CODEX_BLOCK" | grep -q "$gate"; then
45
+ pass "Codex registers $gate"
46
+ else
47
+ fail_case "Codex missing $gate" "blocking gate registered on Claude but absent from the Codex hooks block"
48
+ fi
49
+ done
50
+
51
+ # Specifically pin the regression: task-write-guard must be in the Edit|Write
52
+ # group (where file writes are gated), not merely present somewhere.
53
+ EDIT_GROUP=$(echo "$CODEX_BLOCK" | awk '/matcher: "Edit\|Write"/{f=1} f{print} f&&/\],/{exit}')
54
+ if echo "$EDIT_GROUP" | grep -q "task-write-guard.js"; then
55
+ pass "task-write-guard.js is in the Codex Edit|Write group"
56
+ else
57
+ fail_case "task-write-guard in Edit|Write" "task-write-guard.js not found in the Codex Edit|Write matcher group"
58
+ fi
59
+
60
+ echo ""
61
+ echo "=== Results: $PASS passed, $FAIL failed ==="
62
+ [ "$FAIL" = "0" ]
@@ -0,0 +1,92 @@
1
+ #!/bin/bash
2
+ # Qualia Framework — hooks/secret-guard.js behavior tests
3
+ # The commit-time gate that enforces two constitution non-negotiables:
4
+ # never commit service_role keys; never commit .env files.
5
+ #
6
+ # Run: bash tests/secret-guard.test.sh
7
+
8
+ PASS=0
9
+ FAIL=0
10
+ GUARD="$(cd "$(dirname "$0")/../hooks" && pwd)/secret-guard.js"
11
+ NODE="${NODE:-node}"
12
+
13
+ TMP_DIRS=()
14
+ cleanup() { for d in "${TMP_DIRS[@]}"; do [ -d "$d" ] && rm -rf "$d"; done; }
15
+ trap cleanup EXIT
16
+ mktmp() { local T; T=$(mktemp -d); TMP_DIRS+=("$T"); echo "$T"; }
17
+ pass() { echo " ✓ $1"; PASS=$((PASS + 1)); }
18
+ fail_case() { echo " ✗ $1"; echo " $2"; FAIL=$((FAIL + 1)); }
19
+
20
+ # Build a throwaway git repo with the given file staged; run the guard via a
21
+ # simulated `git commit` PreToolUse payload; echo the exit code.
22
+ run_guard_on_repo() {
23
+ local repo="$1"
24
+ ( cd "$repo" && printf '{"tool_input":{"command":"git commit -m wip"}}' | $NODE "$GUARD" >/tmp/sg.out 2>&1 )
25
+ echo $?
26
+ }
27
+
28
+ echo "secret-guard.test.sh — hooks/secret-guard.js behavioral tests"
29
+ echo ""
30
+
31
+ if [ ! -f "$GUARD" ]; then
32
+ fail_case "secret-guard exists" "$GUARD not found"; echo "=== Results: $PASS passed, $FAIL failed ==="; exit 1
33
+ fi
34
+ pass "secret-guard.js exists"
35
+ $NODE --check "$GUARD" 2>/dev/null && pass "secret-guard.js syntax is valid" || fail_case "syntax" "node --check failed"
36
+
37
+ git --version >/dev/null 2>&1 || { echo " - git unavailable, skipping behavioral tests"; echo "=== Results: $PASS passed, $FAIL failed ==="; [ "$FAIL" = 0 ]; exit $?; }
38
+
39
+ init_repo() {
40
+ local R; R=$(mktmp)
41
+ ( cd "$R" && git init -q && git config user.email t@t.co && git config user.name t ) 2>/dev/null
42
+ echo "$R"
43
+ }
44
+
45
+ # ── Clean staged file → allow (exit 0) ────────────────────────────────
46
+ R=$(init_repo)
47
+ echo "export const x = 1;" > "$R/app.ts"
48
+ ( cd "$R" && git add app.ts ) 2>/dev/null
49
+ RC=$(run_guard_on_repo "$R")
50
+ [ "$RC" = "0" ] && pass "clean staged file → allowed (exit 0)" || fail_case "clean allow" "exit=$RC $(cat /tmp/sg.out)"
51
+
52
+ # ── Staged private key → block (exit 2), value never printed ──────────
53
+ R=$(init_repo)
54
+ printf -- "-----BEGIN RSA PRIVATE KEY-----\nMIIabc123\n-----END RSA PRIVATE KEY-----\n" > "$R/key.pem"
55
+ ( cd "$R" && git add key.pem ) 2>/dev/null
56
+ RC=$(run_guard_on_repo "$R")
57
+ if [ "$RC" = "2" ] && grep -q "key.pem" /tmp/sg.out && ! grep -q "MIIabc123" /tmp/sg.out; then
58
+ pass "staged private key → blocked (exit 2), value not printed"
59
+ else
60
+ fail_case "private key block" "exit=$RC out=$(head -c 160 /tmp/sg.out)"
61
+ fi
62
+
63
+ # ── Staged .env file → block ──────────────────────────────────────────
64
+ R=$(init_repo)
65
+ echo "API_KEY=whatever" > "$R/.env"
66
+ ( cd "$R" && git add -f .env ) 2>/dev/null
67
+ RC=$(run_guard_on_repo "$R")
68
+ [ "$RC" = "2" ] && grep -q "\.env" /tmp/sg.out && pass "staged .env file → blocked (exit 2)" || fail_case ".env block" "exit=$RC out=$(head -c 160 /tmp/sg.out)"
69
+
70
+ # ── .env.example → allowed (template, not a secret) ──────────────────
71
+ R=$(init_repo)
72
+ echo "API_KEY=" > "$R/.env.example"
73
+ ( cd "$R" && git add .env.example ) 2>/dev/null
74
+ RC=$(run_guard_on_repo "$R")
75
+ [ "$RC" = "0" ] && pass ".env.example → allowed (template, not flagged)" || fail_case ".env.example allow" "exit=$RC out=$(head -c 160 /tmp/sg.out)"
76
+
77
+ # ── service_role key assignment → block ──────────────────────────────
78
+ R=$(init_repo)
79
+ echo 'const k = "SUPABASE_SERVICE_ROLE_KEY=abcdef0123456789abcdef0123456789";' > "$R/cfg.ts"
80
+ ( cd "$R" && git add cfg.ts ) 2>/dev/null
81
+ RC=$(run_guard_on_repo "$R")
82
+ [ "$RC" = "2" ] && pass "service_role key assignment → blocked (exit 2)" || fail_case "service_role block" "exit=$RC out=$(head -c 160 /tmp/sg.out)"
83
+
84
+ # ── Non-commit command → self-filter exits 0 silently ────────────────
85
+ R=$(init_repo)
86
+ echo "API_KEY=whatever" > "$R/.env"; ( cd "$R" && git add -f .env ) 2>/dev/null
87
+ RC=$( ( cd "$R" && printf '{"tool_input":{"command":"git status"}}' | $NODE "$GUARD" >/dev/null 2>&1 ); echo $? )
88
+ [ "$RC" = "0" ] && pass "non-commit command (git status) → self-filters, exit 0" || fail_case "self-filter" "exit=$RC"
89
+
90
+ echo ""
91
+ echo "=== Results: $PASS passed, $FAIL failed ==="
92
+ [ "$FAIL" = "0" ]
@@ -1604,6 +1604,41 @@ else
1604
1604
  fail_case "operate handoff ungated" "out=$OUT"
1605
1605
  fi
1606
1606
 
1607
+ # 64. A5 increment failed-status gap closure (regression: A5 sets status
1608
+ # "failed" on a failed verify, unlike the legacy "verified"+fail path).
1609
+ # Asserts: next_command routes to gap closure (not the /qualia dead-end),
1610
+ # re-plan is allowed from "failed", gap_cycles increments, and the
1611
+ # circuit breaker fires at the limit.
1612
+ TMP=$(make_project)
1613
+ make_valid_plan "$TMP" 1 # plan + contract + evidence for phase 1
1614
+ touch "$TMP/.planning/phase-1-verification.md" # fail verify only needs the file to exist
1615
+ (cd "$TMP" && $NODE "$STATE_JS" migrate >/dev/null 2>&1)
1616
+ A5ID=inc-0001-foundation
1617
+ a5() { (cd "$TMP" && $NODE "$STATE_JS" transition --to "$1" ${2:+--verification "$2"} --id "$A5ID" 2>&1); }
1618
+ a5 planned >/dev/null 2>&1 # ensure planned (no-op if already)
1619
+ a5 built >/dev/null 2>&1
1620
+ VF=$(a5 verified fail)
1621
+ if echo "$VF" | grep -q '"status": "failed"' && echo "$VF" | grep -q '"next_command": "/qualia-plan 1 --gaps"'; then
1622
+ pass "A5 failed verify → status=failed, next_command=/qualia-plan 1 --gaps (no dead-end)"
1623
+ else
1624
+ fail_case "A5 failed next_command" "out=$VF"
1625
+ fi
1626
+ RP1=$(a5 planned)
1627
+ if echo "$RP1" | grep -q '"ok": true' && echo "$RP1" | grep -q '"gap_cycles": 1'; then
1628
+ pass "A5 failed → planned gap closure allowed, gap_cycles=1"
1629
+ else
1630
+ fail_case "A5 replan-from-failed" "out=$RP1"
1631
+ fi
1632
+ a5 built >/dev/null 2>&1; a5 verified fail >/dev/null 2>&1
1633
+ RP2=$(a5 planned)
1634
+ a5 built >/dev/null 2>&1; a5 verified fail >/dev/null 2>&1
1635
+ RP3=$(a5 planned)
1636
+ if echo "$RP2" | grep -q '"gap_cycles": 2' && echo "$RP3" | grep -q "GAP_CYCLE_LIMIT"; then
1637
+ pass "A5 gap_cycles=2 then circuit breaker fires (GAP_CYCLE_LIMIT) at default limit"
1638
+ else
1639
+ fail_case "A5 breaker" "rp2=$RP2 rp3=$RP3"
1640
+ fi
1641
+
1607
1642
  # ─── Summary ─────────────────────────────────────────────
1608
1643
  echo ""
1609
1644
  echo "=== Results: $PASS passed, $FAIL failed ==="