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.
- package/README.md +15 -11
- package/agents/builder.md +28 -0
- package/agents/research-synthesizer.md +7 -0
- package/bin/agent-runs.js +233 -0
- package/bin/cli.js +355 -16
- package/bin/install.js +87 -6
- package/bin/knowledge-flush.js +164 -0
- package/bin/knowledge.js +317 -0
- package/bin/plan-contract.js +220 -0
- package/bin/state.js +15 -9
- package/docs/agent-runs.md +273 -0
- package/docs/journey-demo.html +1008 -0
- package/docs/plan-contract.md +321 -0
- package/docs/reviews/v4.1.0-audit.html +1488 -0
- package/docs/reviews/v4.1.0-audit.md +263 -0
- package/hooks/auto-update.js +3 -7
- package/hooks/git-guardrails.js +167 -0
- package/hooks/pre-compact.js +22 -11
- package/hooks/pre-deploy-gate.js +16 -2
- package/hooks/pre-push.js +22 -2
- package/hooks/stop-session-log.js +180 -0
- package/package.json +8 -2
- package/skills/qualia-build/SKILL.md +5 -5
- package/skills/qualia-debug/SKILL.md +1 -1
- package/skills/qualia-design/SKILL.md +15 -0
- package/skills/qualia-flush/SKILL.md +200 -0
- package/skills/qualia-learn/SKILL.md +47 -37
- package/skills/qualia-new/SKILL.md +1 -1
- package/skills/qualia-plan/SKILL.md +3 -2
- package/skills/qualia-postmortem/SKILL.md +238 -0
- package/skills/qualia-quick/SKILL.md +1 -1
- package/skills/qualia-report/SKILL.md +1 -1
- package/skills/qualia-review/SKILL.md +3 -2
- package/skills/qualia-ship/SKILL.md +12 -10
- package/skills/qualia-verify/SKILL.md +60 -0
- package/templates/help.html +13 -7
- package/templates/knowledge/agents.md +71 -0
- package/templates/knowledge/index.md +47 -0
- package/tests/bin.test.sh +322 -12
- package/tests/hooks.test.sh +131 -20
- package/tests/lib.test.sh +217 -0
- package/tests/runner.js +103 -77
- package/tests/state.test.sh +4 -3
package/tests/bin.test.sh
CHANGED
|
@@ -476,10 +476,11 @@ else
|
|
|
476
476
|
fail_case "CLAUDE.md role substitution"
|
|
477
477
|
fi
|
|
478
478
|
|
|
479
|
-
# 31. All
|
|
479
|
+
# 31. All 9 hooks installed (block-env-edit removed in v3.2.0;
|
|
480
|
+
# git-guardrails + stop-session-log added in v4.2.0)
|
|
480
481
|
HOOK_COUNT=$(ls "$TMP/.claude/hooks/"*.js 2>/dev/null | wc -l)
|
|
481
|
-
if [ "$HOOK_COUNT" -eq
|
|
482
|
-
pass "
|
|
482
|
+
if [ "$HOOK_COUNT" -eq 9 ]; then
|
|
483
|
+
pass "9 hooks installed in hooks/"
|
|
483
484
|
else
|
|
484
485
|
fail_case "hook count" "got $HOOK_COUNT"
|
|
485
486
|
fi
|
|
@@ -488,22 +489,24 @@ fi
|
|
|
488
489
|
if [ -f "$TMP/.claude/settings.json" ] \
|
|
489
490
|
&& grep -q '"SessionStart"' "$TMP/.claude/settings.json" \
|
|
490
491
|
&& grep -q '"PreToolUse"' "$TMP/.claude/settings.json" \
|
|
492
|
+
&& grep -q '"Stop"' "$TMP/.claude/settings.json" \
|
|
491
493
|
&& grep -q '"statusLine"' "$TMP/.claude/settings.json"; then
|
|
492
|
-
pass "settings.json has SessionStart, PreToolUse, statusLine"
|
|
494
|
+
pass "settings.json has SessionStart, PreToolUse, Stop, statusLine"
|
|
493
495
|
else
|
|
494
496
|
fail_case "settings.json contents"
|
|
495
497
|
fi
|
|
496
498
|
|
|
497
|
-
# 33. settings.json contains all
|
|
498
|
-
if grep -q '
|
|
499
|
-
&& grep -q 'branch-guard.js' "$TMP/.claude/settings.json" \
|
|
499
|
+
# 33. settings.json contains all 9 hooks wired correctly
|
|
500
|
+
if grep -q 'branch-guard.js' "$TMP/.claude/settings.json" \
|
|
500
501
|
&& grep -q 'migration-guard.js' "$TMP/.claude/settings.json" \
|
|
501
502
|
&& grep -q 'pre-push.js' "$TMP/.claude/settings.json" \
|
|
502
503
|
&& grep -q 'pre-deploy-gate.js' "$TMP/.claude/settings.json" \
|
|
503
504
|
&& grep -q 'auto-update.js' "$TMP/.claude/settings.json" \
|
|
504
505
|
&& grep -q 'session-start.js' "$TMP/.claude/settings.json" \
|
|
505
|
-
&& grep -q 'pre-compact.js' "$TMP/.claude/settings.json"
|
|
506
|
-
|
|
506
|
+
&& grep -q 'pre-compact.js' "$TMP/.claude/settings.json" \
|
|
507
|
+
&& grep -q 'git-guardrails.js' "$TMP/.claude/settings.json" \
|
|
508
|
+
&& grep -q 'stop-session-log.js' "$TMP/.claude/settings.json"; then
|
|
509
|
+
pass "settings.json has all 9 hooks wired"
|
|
507
510
|
else
|
|
508
511
|
fail_case "settings.json missing hooks"
|
|
509
512
|
fi
|
|
@@ -635,8 +638,11 @@ else
|
|
|
635
638
|
fail_case "knowledge idempotency" "exit=$EXIT"
|
|
636
639
|
fi
|
|
637
640
|
|
|
638
|
-
# 43. ERP API key
|
|
639
|
-
|
|
641
|
+
# 43. ERP API key is opt-in and preserved on re-install
|
|
642
|
+
CONFIG_ENABLED=$($NODE -e "const c=require('$TMP/.claude/.qualia-config.json'); console.log(c.erp && c.erp.enabled === false ? 'disabled' : 'enabled')")
|
|
643
|
+
if [ ! -f "$TMP/.claude/.erp-api-key" ] && [ "$CONFIG_ENABLED" = "disabled" ]; then
|
|
644
|
+
echo " ✓ ERP disabled when no API key is provided"
|
|
645
|
+
PASS=$((PASS + 1))
|
|
640
646
|
echo "custom-erp-key" > "$TMP/.claude/.erp-api-key"
|
|
641
647
|
echo "QS-FAWZI-01" | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/out3.log" 2>&1
|
|
642
648
|
if grep -q "custom-erp-key" "$TMP/.claude/.erp-api-key"; then
|
|
@@ -645,7 +651,7 @@ if [ -f "$TMP/.claude/.erp-api-key" ]; then
|
|
|
645
651
|
fail_case ".erp-api-key preservation"
|
|
646
652
|
fi
|
|
647
653
|
else
|
|
648
|
-
fail_case "
|
|
654
|
+
fail_case "ERP opt-in default" "key_exists=$(test -f "$TMP/.claude/.erp-api-key" && echo yes || echo no) config=$CONFIG_ENABLED"
|
|
649
655
|
fi
|
|
650
656
|
|
|
651
657
|
# 44. Templates copied to qualia-templates/
|
|
@@ -682,6 +688,310 @@ else
|
|
|
682
688
|
fail_case "config version mismatch"
|
|
683
689
|
fi
|
|
684
690
|
|
|
691
|
+
echo ""
|
|
692
|
+
echo "--- v4.2.0 phase 3 (flush + forks + model matrix) ---"
|
|
693
|
+
|
|
694
|
+
# 61. qualia-flush skill installs
|
|
695
|
+
TMP=$(mktmp)
|
|
696
|
+
echo "QS-FAWZI-01" | HOME="$TMP" $NODE "$INSTALL_JS" >/dev/null 2>&1
|
|
697
|
+
if [ -f "$TMP/.claude/skills/qualia-flush/SKILL.md" ]; then
|
|
698
|
+
pass "qualia-flush skill installs"
|
|
699
|
+
else
|
|
700
|
+
fail_case "qualia-flush skill missing after install"
|
|
701
|
+
fi
|
|
702
|
+
|
|
703
|
+
# 62. CLAUDE_AGENT_FORK_ENABLED=1 in settings.json
|
|
704
|
+
if grep -q '"CLAUDE_AGENT_FORK_ENABLED": "1"' "$TMP/.claude/settings.json"; then
|
|
705
|
+
pass "settings.env CLAUDE_AGENT_FORK_ENABLED=1 (forked subagents on by default)"
|
|
706
|
+
else
|
|
707
|
+
fail_case "CLAUDE_AGENT_FORK_ENABLED not set"
|
|
708
|
+
fi
|
|
709
|
+
|
|
710
|
+
# 63. research-synthesizer agent has model: haiku frontmatter
|
|
711
|
+
if grep -q '^model: haiku$' "$TMP/.claude/agents/research-synthesizer.md"; then
|
|
712
|
+
pass "research-synthesizer agent uses haiku (model matrix)"
|
|
713
|
+
else
|
|
714
|
+
fail_case "research-synthesizer missing model frontmatter"
|
|
715
|
+
fi
|
|
716
|
+
|
|
717
|
+
# 64. Other agents do NOT have model frontmatter (conservative matrix)
|
|
718
|
+
SAFE_AGENTS=("planner.md" "builder.md" "verifier.md" "plan-checker.md")
|
|
719
|
+
ALL_OK=1
|
|
720
|
+
for a in "${SAFE_AGENTS[@]}"; do
|
|
721
|
+
if grep -q '^model: ' "$TMP/.claude/agents/$a" 2>/dev/null; then
|
|
722
|
+
ALL_OK=0
|
|
723
|
+
fi
|
|
724
|
+
done
|
|
725
|
+
if [ "$ALL_OK" = "1" ]; then
|
|
726
|
+
pass "high-stakes agents (planner/builder/verifier/plan-checker) keep default model"
|
|
727
|
+
else
|
|
728
|
+
fail_case "high-stakes agent has unexpected model frontmatter"
|
|
729
|
+
fi
|
|
730
|
+
|
|
731
|
+
echo ""
|
|
732
|
+
echo "--- knowledge.js (memory-layer loader) ---"
|
|
733
|
+
|
|
734
|
+
KN="$FRAMEWORK_DIR/bin/knowledge.js"
|
|
735
|
+
|
|
736
|
+
# 48. Help
|
|
737
|
+
EXIT=0; OUT=$($NODE "$KN" help 2>&1) || EXIT=$?
|
|
738
|
+
if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "knowledge.js"; then
|
|
739
|
+
pass "help prints usage"
|
|
740
|
+
else
|
|
741
|
+
fail_case "help" "exit=$EXIT"
|
|
742
|
+
fi
|
|
743
|
+
|
|
744
|
+
# 49. Default (no args) → "no entries" stub on fresh install, exit 0
|
|
745
|
+
TMP=$(mktmp)
|
|
746
|
+
EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" 2>&1) || EXIT=$?
|
|
747
|
+
if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "no entries"; then
|
|
748
|
+
pass "default → exits 0 with stub on fresh install"
|
|
749
|
+
else
|
|
750
|
+
fail_case "default no init" "exit=$EXIT"
|
|
751
|
+
fi
|
|
752
|
+
|
|
753
|
+
# 50. With initialized index → returns content
|
|
754
|
+
TMP=$(mktmp)
|
|
755
|
+
mkdir -p "$TMP/.claude/knowledge"
|
|
756
|
+
echo "# Test Index" > "$TMP/.claude/knowledge/index.md"
|
|
757
|
+
EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" 2>&1) || EXIT=$?
|
|
758
|
+
if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "Test Index"; then
|
|
759
|
+
pass "default → prints index.md when present"
|
|
760
|
+
else
|
|
761
|
+
fail_case "default with index" "exit=$EXIT"
|
|
762
|
+
fi
|
|
763
|
+
|
|
764
|
+
# 51. load <alias> resolves to mapped filename
|
|
765
|
+
TMP=$(mktmp)
|
|
766
|
+
mkdir -p "$TMP/.claude/knowledge"
|
|
767
|
+
echo "# Patterns" > "$TMP/.claude/knowledge/learned-patterns.md"
|
|
768
|
+
EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" load patterns 2>&1) || EXIT=$?
|
|
769
|
+
if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "# Patterns"; then
|
|
770
|
+
pass "load patterns → learned-patterns.md"
|
|
771
|
+
else
|
|
772
|
+
fail_case "load alias" "exit=$EXIT"
|
|
773
|
+
fi
|
|
774
|
+
|
|
775
|
+
# 52. load <missing-file> → "no entries" stub, exit 0
|
|
776
|
+
TMP=$(mktmp)
|
|
777
|
+
mkdir -p "$TMP/.claude/knowledge"
|
|
778
|
+
EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" load fixes 2>&1) || EXIT=$?
|
|
779
|
+
if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "no entries"; then
|
|
780
|
+
pass "load missing → exit 0 with stub (skill-pipeable)"
|
|
781
|
+
else
|
|
782
|
+
fail_case "load missing" "exit=$EXIT"
|
|
783
|
+
fi
|
|
784
|
+
|
|
785
|
+
# 53. append a pattern → entry lands on disk
|
|
786
|
+
TMP=$(mktmp)
|
|
787
|
+
EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" append --type pattern --title "RLS rule" --body "Add RLS in same migration as table" 2>&1) || EXIT=$?
|
|
788
|
+
if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "appended"; then
|
|
789
|
+
pass "append pattern → exit 0 with confirmation"
|
|
790
|
+
else
|
|
791
|
+
fail_case "append pattern" "exit=$EXIT"
|
|
792
|
+
fi
|
|
793
|
+
if [ -f "$TMP/.claude/knowledge/learned-patterns.md" ] \
|
|
794
|
+
&& grep -q "### RLS rule" "$TMP/.claude/knowledge/learned-patterns.md" \
|
|
795
|
+
&& grep -q "Add RLS in same migration" "$TMP/.claude/knowledge/learned-patterns.md"; then
|
|
796
|
+
pass "appended entry has title + body"
|
|
797
|
+
else
|
|
798
|
+
fail_case "append content"
|
|
799
|
+
fi
|
|
800
|
+
|
|
801
|
+
# 54. append without --title → exit 1
|
|
802
|
+
EXIT=0; HOME="$TMP" $NODE "$KN" append --type pattern --body "x" >/dev/null 2>&1 || EXIT=$?
|
|
803
|
+
if [ "$EXIT" -eq 1 ]; then
|
|
804
|
+
pass "append missing --title → exit 1"
|
|
805
|
+
else
|
|
806
|
+
fail_case "append missing title" "exit=$EXIT"
|
|
807
|
+
fi
|
|
808
|
+
|
|
809
|
+
# 55. append with bad type → exit 1
|
|
810
|
+
EXIT=0; HOME="$TMP" $NODE "$KN" append --type bogus --title T --body B >/dev/null 2>&1 || EXIT=$?
|
|
811
|
+
if [ "$EXIT" -eq 1 ]; then
|
|
812
|
+
pass "append bad --type → exit 1"
|
|
813
|
+
else
|
|
814
|
+
fail_case "append bad type" "exit=$EXIT"
|
|
815
|
+
fi
|
|
816
|
+
|
|
817
|
+
# 56. search finds an appended entry
|
|
818
|
+
TMP=$(mktmp)
|
|
819
|
+
HOME="$TMP" $NODE "$KN" append --type fix --title "Vercel build crash" --body "use node 20.x in package.json engines" >/dev/null 2>&1
|
|
820
|
+
EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" search "Vercel" 2>&1) || EXIT=$?
|
|
821
|
+
if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "Vercel"; then
|
|
822
|
+
pass "search finds appended entries"
|
|
823
|
+
else
|
|
824
|
+
fail_case "search appended" "exit=$EXIT"
|
|
825
|
+
fi
|
|
826
|
+
|
|
827
|
+
# 57. search with no matches → "no matches" stub, exit 0
|
|
828
|
+
EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" search "xyzzy_nonexistent_12345" 2>&1) || EXIT=$?
|
|
829
|
+
if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "no matches"; then
|
|
830
|
+
pass "search no matches → exit 0 with stub"
|
|
831
|
+
else
|
|
832
|
+
fail_case "search no matches" "exit=$EXIT"
|
|
833
|
+
fi
|
|
834
|
+
|
|
835
|
+
# 58. list shows existing files
|
|
836
|
+
TMP=$(mktmp)
|
|
837
|
+
mkdir -p "$TMP/.claude/knowledge"
|
|
838
|
+
echo "x" > "$TMP/.claude/knowledge/foo.md"
|
|
839
|
+
EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" list 2>&1) || EXIT=$?
|
|
840
|
+
if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "foo.md"; then
|
|
841
|
+
pass "list shows existing files"
|
|
842
|
+
else
|
|
843
|
+
fail_case "list" "exit=$EXIT"
|
|
844
|
+
fi
|
|
845
|
+
|
|
846
|
+
# 59. unknown command falls through to load (`knowledge.js patterns` shorthand)
|
|
847
|
+
TMP=$(mktmp)
|
|
848
|
+
mkdir -p "$TMP/.claude/knowledge"
|
|
849
|
+
echo "# Patterns content" > "$TMP/.claude/knowledge/learned-patterns.md"
|
|
850
|
+
EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" patterns 2>&1) || EXIT=$?
|
|
851
|
+
if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "# Patterns content"; then
|
|
852
|
+
pass "unknown command → falls through to load"
|
|
853
|
+
else
|
|
854
|
+
fail_case "fallthrough" "exit=$EXIT"
|
|
855
|
+
fi
|
|
856
|
+
|
|
857
|
+
# 60. path command resolves alias to absolute path (no read)
|
|
858
|
+
EXIT=0; OUT=$($NODE "$KN" path patterns 2>&1) || EXIT=$?
|
|
859
|
+
if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "learned-patterns.md"; then
|
|
860
|
+
pass "path resolves alias to filename"
|
|
861
|
+
else
|
|
862
|
+
fail_case "path" "exit=$EXIT"
|
|
863
|
+
fi
|
|
864
|
+
|
|
865
|
+
echo ""
|
|
866
|
+
echo "--- v4.3.0 (loader subdirs + self-healing skills) ---"
|
|
867
|
+
|
|
868
|
+
# 65. Subdirectory-qualified path: knowledge.js load concepts/foo
|
|
869
|
+
TMP=$(mktmp)
|
|
870
|
+
mkdir -p "$TMP/.claude/knowledge/concepts"
|
|
871
|
+
echo "# Stripe checkout" > "$TMP/.claude/knowledge/concepts/stripe.md"
|
|
872
|
+
EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" load concepts/stripe 2>&1) || EXIT=$?
|
|
873
|
+
if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "Stripe checkout"; then
|
|
874
|
+
pass "load concepts/stripe → reads concepts/stripe.md"
|
|
875
|
+
else
|
|
876
|
+
fail_case "subdir-qualified load" "exit=$EXIT"
|
|
877
|
+
fi
|
|
878
|
+
|
|
879
|
+
# 66. Bare name auto-discovers in concepts/
|
|
880
|
+
TMP=$(mktmp)
|
|
881
|
+
mkdir -p "$TMP/.claude/knowledge/concepts"
|
|
882
|
+
echo "# Voice agent state" > "$TMP/.claude/knowledge/concepts/voice-agent-call-state.md"
|
|
883
|
+
EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" load voice-agent-call-state 2>&1) || EXIT=$?
|
|
884
|
+
if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "Voice agent state"; then
|
|
885
|
+
pass "load <bare> auto-discovers in concepts/"
|
|
886
|
+
else
|
|
887
|
+
fail_case "bare-name subdir discovery" "exit=$EXIT"
|
|
888
|
+
fi
|
|
889
|
+
|
|
890
|
+
# 67. Top-level wins when both top-level and subdir have same filename
|
|
891
|
+
TMP=$(mktmp)
|
|
892
|
+
mkdir -p "$TMP/.claude/knowledge/concepts"
|
|
893
|
+
echo "# Top level" > "$TMP/.claude/knowledge/foo.md"
|
|
894
|
+
echo "# Subdir version" > "$TMP/.claude/knowledge/concepts/foo.md"
|
|
895
|
+
EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" load foo 2>&1) || EXIT=$?
|
|
896
|
+
if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "Top level" && ! echo "$OUT" | grep -q "Subdir version"; then
|
|
897
|
+
pass "top-level wins over subdir on bare-name lookup"
|
|
898
|
+
else
|
|
899
|
+
fail_case "precedence" "exit=$EXIT"
|
|
900
|
+
fi
|
|
901
|
+
|
|
902
|
+
# 68. Subdir-qualified still works when top-level exists with same name
|
|
903
|
+
EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" load concepts/foo 2>&1) || EXIT=$?
|
|
904
|
+
if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "Subdir version"; then
|
|
905
|
+
pass "qualified path overrides top-level"
|
|
906
|
+
else
|
|
907
|
+
fail_case "qualified override" "exit=$EXIT"
|
|
908
|
+
fi
|
|
909
|
+
|
|
910
|
+
# 69. Search recurses into subdirs
|
|
911
|
+
TMP=$(mktmp)
|
|
912
|
+
mkdir -p "$TMP/.claude/knowledge/concepts"
|
|
913
|
+
echo "supabase RLS pattern" > "$TMP/.claude/knowledge/concepts/auth.md"
|
|
914
|
+
EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" search RLS 2>&1) || EXIT=$?
|
|
915
|
+
if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "concepts/auth.md"; then
|
|
916
|
+
pass "search recurses into concepts/ subdir"
|
|
917
|
+
else
|
|
918
|
+
fail_case "search subdir" "exit=$EXIT"
|
|
919
|
+
fi
|
|
920
|
+
|
|
921
|
+
# 70. qualia-postmortem skill installs
|
|
922
|
+
TMP=$(mktmp)
|
|
923
|
+
echo "QS-FAWZI-01" | HOME="$TMP" $NODE "$INSTALL_JS" >/dev/null 2>&1
|
|
924
|
+
if [ -f "$TMP/.claude/skills/qualia-postmortem/SKILL.md" ]; then
|
|
925
|
+
pass "qualia-postmortem skill installs"
|
|
926
|
+
else
|
|
927
|
+
fail_case "qualia-postmortem skill missing"
|
|
928
|
+
fi
|
|
929
|
+
|
|
930
|
+
# 71. /qualia-verify skill documents --adversarial flag
|
|
931
|
+
if grep -qF -- '--adversarial' "$TMP/.claude/skills/qualia-verify/SKILL.md"; then
|
|
932
|
+
pass "qualia-verify documents --adversarial flag"
|
|
933
|
+
else
|
|
934
|
+
fail_case "qualia-verify missing --adversarial"
|
|
935
|
+
fi
|
|
936
|
+
|
|
937
|
+
# 72. /qualia-verify wires /qualia-postmortem on FAIL
|
|
938
|
+
if grep -q 'qualia-postmortem' "$TMP/.claude/skills/qualia-verify/SKILL.md"; then
|
|
939
|
+
pass "qualia-verify wires /qualia-postmortem on FAIL"
|
|
940
|
+
else
|
|
941
|
+
fail_case "qualia-verify missing postmortem wiring"
|
|
942
|
+
fi
|
|
943
|
+
|
|
944
|
+
# 73. knowledge-flush.js installs at ~/.claude/bin/
|
|
945
|
+
if [ -f "$TMP/.claude/bin/knowledge-flush.js" ]; then
|
|
946
|
+
pass "knowledge-flush.js installs"
|
|
947
|
+
else
|
|
948
|
+
fail_case "knowledge-flush.js missing after install"
|
|
949
|
+
fi
|
|
950
|
+
|
|
951
|
+
# 74. knowledge-flush.js is syntactically valid Node
|
|
952
|
+
EXIT=0; $NODE -c "$TMP/.claude/bin/knowledge-flush.js" 2>/dev/null || EXIT=$?
|
|
953
|
+
if [ "$EXIT" -eq 0 ]; then
|
|
954
|
+
pass "knowledge-flush.js parses as valid Node"
|
|
955
|
+
else
|
|
956
|
+
fail_case "knowledge-flush.js parse error"
|
|
957
|
+
fi
|
|
958
|
+
|
|
959
|
+
# 75. knowledge-flush.js exits 0 with no daily-log (cron-friendly: no spam)
|
|
960
|
+
TMP_HOME=$(mktmp)
|
|
961
|
+
mkdir -p "$TMP_HOME/.claude/knowledge"
|
|
962
|
+
EXIT=0; HOME="$TMP_HOME" $NODE "$FRAMEWORK_DIR/bin/knowledge-flush.js" >/dev/null 2>&1 || EXIT=$?
|
|
963
|
+
if [ "$EXIT" -eq 0 ]; then
|
|
964
|
+
pass "knowledge-flush exits 0 with no daily-log (no cron spam)"
|
|
965
|
+
else
|
|
966
|
+
fail_case "knowledge-flush exited $EXIT with no daily-log"
|
|
967
|
+
fi
|
|
968
|
+
|
|
969
|
+
# 76. knowledge-flush.js writes structured JSONL log even on skip
|
|
970
|
+
TMP_HOME=$(mktmp)
|
|
971
|
+
HOME="$TMP_HOME" $NODE "$FRAMEWORK_DIR/bin/knowledge-flush.js" >/dev/null 2>&1
|
|
972
|
+
if [ -f "$TMP_HOME/.claude/.qualia-flush.log" ] && grep -q '"event":"skipped"' "$TMP_HOME/.claude/.qualia-flush.log"; then
|
|
973
|
+
pass "knowledge-flush writes JSONL audit log"
|
|
974
|
+
else
|
|
975
|
+
fail_case "knowledge-flush no audit log"
|
|
976
|
+
fi
|
|
977
|
+
|
|
978
|
+
# 77. CLI `qualia-framework flush` dispatches to the script (errors when not installed)
|
|
979
|
+
TMP_HOME=$(mktmp)
|
|
980
|
+
EXIT=0; OUT=$(HOME="$TMP_HOME" $NODE "$CLI_JS" flush 2>&1) || EXIT=$?
|
|
981
|
+
if [ "$EXIT" -eq 1 ] && echo "$OUT" | grep -q "knowledge-flush.js not installed"; then
|
|
982
|
+
pass "CLI flush command surfaces install error gracefully"
|
|
983
|
+
else
|
|
984
|
+
fail_case "CLI flush dispatch" "exit=$EXIT out=$OUT"
|
|
985
|
+
fi
|
|
986
|
+
|
|
987
|
+
# 78. CLI flush command appears in help text
|
|
988
|
+
EXIT=0; OUT=$($NODE "$CLI_JS" help 2>&1) || EXIT=$?
|
|
989
|
+
if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "flush"; then
|
|
990
|
+
pass "help text documents flush command"
|
|
991
|
+
else
|
|
992
|
+
fail_case "help missing flush" "exit=$EXIT"
|
|
993
|
+
fi
|
|
994
|
+
|
|
685
995
|
echo ""
|
|
686
996
|
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
687
997
|
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
package/tests/hooks.test.sh
CHANGED
|
@@ -34,28 +34,17 @@ for f in "$HOOKS_DIR"/*.js; do
|
|
|
34
34
|
fi
|
|
35
35
|
done
|
|
36
36
|
|
|
37
|
-
# --- block-env-edit.js ---
|
|
37
|
+
# --- block-env-edit.js retired in v3.2.0 ---
|
|
38
38
|
echo ""
|
|
39
39
|
echo "block-env-edit:"
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
assert_exit "blocks .env" 2 $?
|
|
49
|
-
|
|
50
|
-
# Windows-style path with backslashes (normalized by the hook)
|
|
51
|
-
echo '{"tool_input":{"file_path":"C:\\project\\.env.local"}}' | $NODE "$HOOKS_DIR/block-env-edit.js" > /dev/null 2>&1
|
|
52
|
-
assert_exit "blocks windows .env.local" 2 $?
|
|
53
|
-
|
|
54
|
-
echo '{"tool_input":{"file_path":"src/app.tsx"}}' | $NODE "$HOOKS_DIR/block-env-edit.js" > /dev/null 2>&1
|
|
55
|
-
assert_exit "allows src/app.tsx" 0 $?
|
|
56
|
-
|
|
57
|
-
echo '{"tool_input":{"file_path":"components/Footer.tsx"}}' | $NODE "$HOOKS_DIR/block-env-edit.js" > /dev/null 2>&1
|
|
58
|
-
assert_exit "allows components/Footer.tsx" 0 $?
|
|
41
|
+
if [ ! -f "$HOOKS_DIR/block-env-edit.js" ]; then
|
|
42
|
+
echo " ✓ retired hook is absent"
|
|
43
|
+
PASS=$((PASS + 1))
|
|
44
|
+
else
|
|
45
|
+
echo " ✗ retired hook still exists"
|
|
46
|
+
FAIL=$((FAIL + 1))
|
|
47
|
+
fi
|
|
59
48
|
|
|
60
49
|
# --- migration-guard.js ---
|
|
61
50
|
echo ""
|
|
@@ -329,7 +318,7 @@ mkdir -p "$TMP/app/admin"
|
|
|
329
318
|
echo 'const key = "service_role"; export default function Page() { return <div>{key}</div>; }' > "$TMP/app/admin/page.tsx"
|
|
330
319
|
OUT=$( (cd "$TMP" && $NODE "$HOOKS_DIR/pre-deploy-gate.js") 2>&1 )
|
|
331
320
|
RC=$?
|
|
332
|
-
assert_exit "regular page.tsx with service_role → blocked (exit
|
|
321
|
+
assert_exit "regular page.tsx with service_role → blocked (exit 2)" 2 $RC
|
|
333
322
|
rm -rf "$TMP"
|
|
334
323
|
|
|
335
324
|
# --- session-start.js — must exit 0 always ---
|
|
@@ -379,6 +368,128 @@ else
|
|
|
379
368
|
fi
|
|
380
369
|
rm -rf "$TMP"
|
|
381
370
|
|
|
371
|
+
# --- git-guardrails.js ---
|
|
372
|
+
echo ""
|
|
373
|
+
echo "git-guardrails:"
|
|
374
|
+
|
|
375
|
+
# No stdin / no command → allow (exit 0)
|
|
376
|
+
echo '{}' | $NODE "$HOOKS_DIR/git-guardrails.js" > /dev/null 2>&1
|
|
377
|
+
assert_exit "no command → allowed" 0 $?
|
|
378
|
+
|
|
379
|
+
# Force push to main → BLOCK
|
|
380
|
+
echo '{"tool_input":{"command":"git push --force origin main"}}' | $NODE "$HOOKS_DIR/git-guardrails.js" > /dev/null 2>&1
|
|
381
|
+
assert_exit "force-push to main → blocked" 2 $?
|
|
382
|
+
|
|
383
|
+
echo '{"tool_input":{"command":"git push -f origin master"}}' | $NODE "$HOOKS_DIR/git-guardrails.js" > /dev/null 2>&1
|
|
384
|
+
assert_exit "force-push (-f) to master → blocked" 2 $?
|
|
385
|
+
|
|
386
|
+
# --force-with-lease to main → ALLOW (the safe variant)
|
|
387
|
+
echo '{"tool_input":{"command":"git push --force-with-lease origin main"}}' | $NODE "$HOOKS_DIR/git-guardrails.js" > /dev/null 2>&1
|
|
388
|
+
assert_exit "--force-with-lease to main → allowed" 0 $?
|
|
389
|
+
|
|
390
|
+
# Regular push → ALLOW
|
|
391
|
+
echo '{"tool_input":{"command":"git push origin feature/x"}}' | $NODE "$HOOKS_DIR/git-guardrails.js" > /dev/null 2>&1
|
|
392
|
+
assert_exit "regular push → allowed" 0 $?
|
|
393
|
+
|
|
394
|
+
# git clean -fd → BLOCK
|
|
395
|
+
echo '{"tool_input":{"command":"git clean -fd"}}' | $NODE "$HOOKS_DIR/git-guardrails.js" > /dev/null 2>&1
|
|
396
|
+
assert_exit "git clean -fd → blocked" 2 $?
|
|
397
|
+
|
|
398
|
+
echo '{"tool_input":{"command":"git clean -fdx"}}' | $NODE "$HOOKS_DIR/git-guardrails.js" > /dev/null 2>&1
|
|
399
|
+
assert_exit "git clean -fdx → blocked" 2 $?
|
|
400
|
+
|
|
401
|
+
# git branch -D main → BLOCK
|
|
402
|
+
echo '{"tool_input":{"command":"git branch -D main"}}' | $NODE "$HOOKS_DIR/git-guardrails.js" > /dev/null 2>&1
|
|
403
|
+
assert_exit "git branch -D main → blocked" 2 $?
|
|
404
|
+
|
|
405
|
+
# rm -rf .git → BLOCK
|
|
406
|
+
echo '{"tool_input":{"command":"rm -rf .git"}}' | $NODE "$HOOKS_DIR/git-guardrails.js" > /dev/null 2>&1
|
|
407
|
+
assert_exit "rm -rf .git → blocked" 2 $?
|
|
408
|
+
|
|
409
|
+
# rm -rf src → ALLOW (only .git is special)
|
|
410
|
+
echo '{"tool_input":{"command":"rm -rf src/old"}}' | $NODE "$HOOKS_DIR/git-guardrails.js" > /dev/null 2>&1
|
|
411
|
+
assert_exit "rm -rf src/old → allowed" 0 $?
|
|
412
|
+
|
|
413
|
+
# QUALIA_ALLOW_DESTRUCTIVE=1 escape hatch → ALLOW even force push
|
|
414
|
+
echo '{"tool_input":{"command":"git push --force origin main"}}' | QUALIA_ALLOW_DESTRUCTIVE=1 $NODE "$HOOKS_DIR/git-guardrails.js" > /dev/null 2>&1
|
|
415
|
+
assert_exit "QUALIA_ALLOW_DESTRUCTIVE=1 → allowed despite force-push" 0 $?
|
|
416
|
+
|
|
417
|
+
# Block reason includes "BLOCKED" + "main" for force push to main
|
|
418
|
+
OUT=$(echo '{"tool_input":{"command":"git push --force origin main"}}' | $NODE "$HOOKS_DIR/git-guardrails.js" 2>&1)
|
|
419
|
+
if echo "$OUT" | grep -q "BLOCKED" && echo "$OUT" | grep -q "main"; then
|
|
420
|
+
echo " ✓ block reason includes BLOCKED + main"
|
|
421
|
+
PASS=$((PASS + 1))
|
|
422
|
+
else
|
|
423
|
+
echo " ✗ block reason missing BLOCKED or main: $OUT"
|
|
424
|
+
FAIL=$((FAIL + 1))
|
|
425
|
+
fi
|
|
426
|
+
|
|
427
|
+
# --- stop-session-log.js ---
|
|
428
|
+
echo ""
|
|
429
|
+
echo "stop-session-log:"
|
|
430
|
+
|
|
431
|
+
# Outside a git repo, with nothing to log → exit 0 silent
|
|
432
|
+
TMP=$(mktemp -d)
|
|
433
|
+
HOME="$TMP" $NODE "$HOOKS_DIR/stop-session-log.js" >/dev/null 2>&1
|
|
434
|
+
assert_exit "no activity → exit 0 silent" 0 $?
|
|
435
|
+
# Daily log dir might or might not exist; the hook must NOT crash either way.
|
|
436
|
+
rm -rf "$TMP"
|
|
437
|
+
|
|
438
|
+
# In a git repo with .planning/tracking.json → writes to daily log
|
|
439
|
+
TMP=$(mktemp -d)
|
|
440
|
+
mkdir -p "$TMP/proj/.planning"
|
|
441
|
+
(cd "$TMP/proj" && git init -q && git checkout -b feat/test -q 2>/dev/null && git config user.email t@t.com && git config user.name T)
|
|
442
|
+
echo "x" > "$TMP/proj/file.txt"
|
|
443
|
+
(cd "$TMP/proj" && git add file.txt && git commit -q -m "seed" 2>/dev/null)
|
|
444
|
+
cat > "$TMP/proj/.planning/tracking.json" <<EOF
|
|
445
|
+
{"phase":2,"phase_total":4,"tasks_done":3,"tasks_total":5}
|
|
446
|
+
EOF
|
|
447
|
+
# Touch a file so the diff has something
|
|
448
|
+
echo "y" >> "$TMP/proj/file.txt"
|
|
449
|
+
(cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/stop-session-log.js" >/dev/null 2>&1)
|
|
450
|
+
assert_exit "with activity → exit 0" 0 $?
|
|
451
|
+
TODAY=$(date -u +%Y-%m-%d)
|
|
452
|
+
LOG_FILE="$TMP/.claude/knowledge/daily-log/$TODAY.md"
|
|
453
|
+
if [ -f "$LOG_FILE" ] && grep -q "phase=2/4" "$LOG_FILE" && grep -q "tasks=3/5" "$LOG_FILE"; then
|
|
454
|
+
echo " ✓ daily-log contains phase + tasks"
|
|
455
|
+
PASS=$((PASS + 1))
|
|
456
|
+
else
|
|
457
|
+
echo " ✗ daily-log missing or malformed: $(cat "$LOG_FILE" 2>/dev/null)"
|
|
458
|
+
FAIL=$((FAIL + 1))
|
|
459
|
+
fi
|
|
460
|
+
# Header is project name (basename of repo root)
|
|
461
|
+
if [ -f "$LOG_FILE" ] && grep -q "^## proj$" "$LOG_FILE"; then
|
|
462
|
+
echo " ✓ daily-log has project header"
|
|
463
|
+
PASS=$((PASS + 1))
|
|
464
|
+
else
|
|
465
|
+
echo " ✗ daily-log missing project header"
|
|
466
|
+
FAIL=$((FAIL + 1))
|
|
467
|
+
fi
|
|
468
|
+
rm -rf "$TMP"
|
|
469
|
+
|
|
470
|
+
# Stop hook is idempotent within MIN_INTERVAL_MS — second run within 5min skips
|
|
471
|
+
TMP=$(mktemp -d)
|
|
472
|
+
mkdir -p "$TMP/proj/.planning" "$TMP/.claude"
|
|
473
|
+
(cd "$TMP/proj" && git init -q && git checkout -b feat/x -q 2>/dev/null && git config user.email t@t.com && git config user.name T)
|
|
474
|
+
echo "z" > "$TMP/proj/f.txt"
|
|
475
|
+
(cd "$TMP/proj" && git add f.txt && git commit -q -m "s" 2>/dev/null)
|
|
476
|
+
echo '{"phase":1,"phase_total":2,"tasks_done":1,"tasks_total":2}' > "$TMP/proj/.planning/tracking.json"
|
|
477
|
+
echo "a" >> "$TMP/proj/f.txt"
|
|
478
|
+
# First run — writes
|
|
479
|
+
(cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/stop-session-log.js" >/dev/null 2>&1)
|
|
480
|
+
LINES_BEFORE=$(wc -l < "$TMP/.claude/knowledge/daily-log/$(date -u +%Y-%m-%d).md" 2>/dev/null || echo 0)
|
|
481
|
+
# Second run within 5min — must skip
|
|
482
|
+
(cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/stop-session-log.js" >/dev/null 2>&1)
|
|
483
|
+
LINES_AFTER=$(wc -l < "$TMP/.claude/knowledge/daily-log/$(date -u +%Y-%m-%d).md" 2>/dev/null || echo 0)
|
|
484
|
+
if [ "$LINES_BEFORE" = "$LINES_AFTER" ]; then
|
|
485
|
+
echo " ✓ second run within MIN_INTERVAL skips (no double-write)"
|
|
486
|
+
PASS=$((PASS + 1))
|
|
487
|
+
else
|
|
488
|
+
echo " ✗ second run wrote anyway ($LINES_BEFORE → $LINES_AFTER)"
|
|
489
|
+
FAIL=$((FAIL + 1))
|
|
490
|
+
fi
|
|
491
|
+
rm -rf "$TMP"
|
|
492
|
+
|
|
382
493
|
echo ""
|
|
383
494
|
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
384
495
|
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|