qualia-framework 4.1.1 → 4.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.
@@ -0,0 +1,71 @@
1
+ # Memory Layer — How This Works
2
+
3
+ You are operating inside the **Qualia Framework memory layer**. This file
4
+ describes the system you're in so you can navigate it deliberately. Read this
5
+ once at session start.
6
+
7
+ ## What's here
8
+
9
+ `~/.claude/knowledge/` is the project-spanning memory tier. It holds three
10
+ kinds of files, each with a clear purpose. Treat the structure as a contract.
11
+
12
+ ```
13
+ ~/.claude/knowledge/
14
+ ├── agents.md ← this file (system overview)
15
+ ├── index.md ← entry point — start here when answering questions
16
+ ├── daily-log/
17
+ │ └── YYYY-MM-DD.md ← raw session checkpoints (auto-written by Stop hook)
18
+ ├── concepts/ ← (future) promoted, durable patterns
19
+ ├── connections/ ← (future) cross-references between concepts
20
+ ├── learned-patterns.md ← curated patterns from /qualia-learn
21
+ ├── common-fixes.md ← recurring fix recipes
22
+ ├── supabase-patterns.md ← Supabase-specific patterns
23
+ ├── voice-agent-patterns.md
24
+ ├── deployment-map.md
25
+ └── employees.md ← team roster
26
+ ```
27
+
28
+ ## How to use it
29
+
30
+ **To answer a question that might be in memory:**
31
+ 1. Read `index.md` first. It tells you which file is likely to have what you
32
+ need. Do **not** scan every file — that defeats the index.
33
+ 2. Follow the index to one or two specific files. Read those.
34
+ 3. If the answer is not there, say so and (when the user agrees) add it via
35
+ `/qualia-learn`.
36
+
37
+ **To remember something new:**
38
+ - Use `/qualia-learn`. It writes to the right tier (pattern vs. fix vs.
39
+ client preference) and updates the index.
40
+ - Do not write to these files directly without an explicit user instruction —
41
+ the index will fall out of sync.
42
+
43
+ **Do not pretend something is in memory if it is not.** Better to say
44
+ "INSUFFICIENT EVIDENCE: searched index.md and learned-patterns.md, no entry
45
+ matches" than to hallucinate a recalled pattern. The grounding protocol
46
+ (`~/.claude/rules/grounding.md`) applies here too.
47
+
48
+ ## Tiers
49
+
50
+ The memory layer follows a Karpathy-style **raw → wiki** progression. Most of
51
+ this is still being built — v4.2.0 ships the daily-log raw tier; v4.3.0 will
52
+ add the LLM-driven flush job that promotes raw entries into concepts and
53
+ connections.
54
+
55
+ | Tier | Files | How it's written | When to read |
56
+ |------|-------|------------------|--------------|
57
+ | Raw | `daily-log/*.md` | Stop hook (auto, mechanical) | Resuming a recent session, debugging a regression |
58
+ | Curated | `learned-patterns.md`, `common-fixes.md`, `*-patterns.md` | `/qualia-learn` (manual, deliberate) | Answering "how do we usually do X?" |
59
+ | Index | `index.md` | `/qualia-learn` updates it; auto-rebuilt by `bin/knowledge.js` | Always read first |
60
+
61
+ ## Cross-cutting rules
62
+
63
+ - **Stale data is dangerous.** If a memory file has not been touched in
64
+ months and the codebase has changed, the memory may be lying. Verify
65
+ current state in the actual files before recommending anything based on
66
+ memory.
67
+ - **Project memory ≠ global memory.** Project-specific decisions belong in
68
+ that project's `.planning/` directory, not here. This directory is for
69
+ patterns that apply across multiple projects.
70
+ - **Never put secrets here.** API keys, tokens, passwords — never. The
71
+ knowledge layer is plain markdown checked into a non-encrypted directory.
@@ -0,0 +1,47 @@
1
+ # Knowledge Index
2
+
3
+ Entry point for `~/.claude/knowledge/`. When answering a question, **read this
4
+ file first**, then jump to the specific file(s) that match.
5
+
6
+ > Auto-maintained by `/qualia-learn`. Do not hand-edit unless the file is out
7
+ > of sync (e.g. after a manual move). Last manual edit: framework install.
8
+
9
+ ## What's where
10
+
11
+ | If the user asks about… | Read… |
12
+ |--------------------------|-------|
13
+ | "How do we usually X?" / patterns we've used before | `learned-patterns.md` |
14
+ | Recurring bug + fix recipes | `common-fixes.md` |
15
+ | Supabase auth, RLS, migrations, edge functions | `supabase-patterns.md` |
16
+ | Retell, ElevenLabs, voice agent flows | `voice-agent-patterns.md` |
17
+ | Where a project is deployed, env vars, domains | `deployment-map.md` |
18
+ | Who is on the team, their role, their access | `employees.md` |
19
+ | What I worked on yesterday / last week | `daily-log/YYYY-MM-DD.md` |
20
+ | Memory layer architecture itself | `agents.md` |
21
+
22
+ ## Daily log conventions
23
+
24
+ `daily-log/YYYY-MM-DD.md` is raw, mechanical, and append-only. Each line is a
25
+ single Stop-hook checkpoint with project, branch, phase, task counts, commit
26
+ count, and up to 3 touched files. **Do not promote daily-log content into the
27
+ curated tier by hand** — the upcoming `bin/knowledge-flush.js` will do that
28
+ deliberately. Hand-promoted entries break the source-of-truth invariant.
29
+
30
+ ## Adding new knowledge
31
+
32
+ Use `/qualia-learn` with the type that matches:
33
+
34
+ - `pattern` → `learned-patterns.md`
35
+ - `fix` → `common-fixes.md`
36
+ - `client preference` → the relevant project's `.planning/`, **not** this
37
+ directory
38
+ - `team member info` → `employees.md`
39
+
40
+ If a new top-level file is needed (e.g. a new technology stack), update this
41
+ index in the same commit.
42
+
43
+ ## Empty / new-install state
44
+
45
+ If a tier file does not exist yet, that means we have not learned anything in
46
+ that domain yet. Don't pretend we have. Either say "no entries" or, if the
47
+ user is asking you to learn it now, run `/qualia-learn`.
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 8 hooks installed
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 8 ]; then
482
- pass "8 hooks installed in hooks/"
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 8 hooks wired correctly
498
- if grep -q 'block-env-edit.js' "$TMP/.claude/settings.json" \
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"; then
506
- pass "settings.json has all 8 hooks wired"
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
@@ -682,6 +685,310 @@ else
682
685
  fail_case "config version mismatch"
683
686
  fi
684
687
 
688
+ echo ""
689
+ echo "--- v4.2.0 phase 3 (flush + forks + model matrix) ---"
690
+
691
+ # 61. qualia-flush skill installs
692
+ TMP=$(mktmp)
693
+ echo "QS-FAWZI-01" | HOME="$TMP" $NODE "$INSTALL_JS" >/dev/null 2>&1
694
+ if [ -f "$TMP/.claude/skills/qualia-flush/SKILL.md" ]; then
695
+ pass "qualia-flush skill installs"
696
+ else
697
+ fail_case "qualia-flush skill missing after install"
698
+ fi
699
+
700
+ # 62. CLAUDE_AGENT_FORK_ENABLED=1 in settings.json
701
+ if grep -q '"CLAUDE_AGENT_FORK_ENABLED": "1"' "$TMP/.claude/settings.json"; then
702
+ pass "settings.env CLAUDE_AGENT_FORK_ENABLED=1 (forked subagents on by default)"
703
+ else
704
+ fail_case "CLAUDE_AGENT_FORK_ENABLED not set"
705
+ fi
706
+
707
+ # 63. research-synthesizer agent has model: haiku frontmatter
708
+ if grep -q '^model: haiku$' "$TMP/.claude/agents/research-synthesizer.md"; then
709
+ pass "research-synthesizer agent uses haiku (model matrix)"
710
+ else
711
+ fail_case "research-synthesizer missing model frontmatter"
712
+ fi
713
+
714
+ # 64. Other agents do NOT have model frontmatter (conservative matrix)
715
+ SAFE_AGENTS=("planner.md" "builder.md" "verifier.md" "plan-checker.md")
716
+ ALL_OK=1
717
+ for a in "${SAFE_AGENTS[@]}"; do
718
+ if grep -q '^model: ' "$TMP/.claude/agents/$a" 2>/dev/null; then
719
+ ALL_OK=0
720
+ fi
721
+ done
722
+ if [ "$ALL_OK" = "1" ]; then
723
+ pass "high-stakes agents (planner/builder/verifier/plan-checker) keep default model"
724
+ else
725
+ fail_case "high-stakes agent has unexpected model frontmatter"
726
+ fi
727
+
728
+ echo ""
729
+ echo "--- knowledge.js (memory-layer loader) ---"
730
+
731
+ KN="$FRAMEWORK_DIR/bin/knowledge.js"
732
+
733
+ # 48. Help
734
+ EXIT=0; OUT=$($NODE "$KN" help 2>&1) || EXIT=$?
735
+ if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "knowledge.js"; then
736
+ pass "help prints usage"
737
+ else
738
+ fail_case "help" "exit=$EXIT"
739
+ fi
740
+
741
+ # 49. Default (no args) → "no entries" stub on fresh install, exit 0
742
+ TMP=$(mktmp)
743
+ EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" 2>&1) || EXIT=$?
744
+ if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "no entries"; then
745
+ pass "default → exits 0 with stub on fresh install"
746
+ else
747
+ fail_case "default no init" "exit=$EXIT"
748
+ fi
749
+
750
+ # 50. With initialized index → returns content
751
+ TMP=$(mktmp)
752
+ mkdir -p "$TMP/.claude/knowledge"
753
+ echo "# Test Index" > "$TMP/.claude/knowledge/index.md"
754
+ EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" 2>&1) || EXIT=$?
755
+ if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "Test Index"; then
756
+ pass "default → prints index.md when present"
757
+ else
758
+ fail_case "default with index" "exit=$EXIT"
759
+ fi
760
+
761
+ # 51. load <alias> resolves to mapped filename
762
+ TMP=$(mktmp)
763
+ mkdir -p "$TMP/.claude/knowledge"
764
+ echo "# Patterns" > "$TMP/.claude/knowledge/learned-patterns.md"
765
+ EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" load patterns 2>&1) || EXIT=$?
766
+ if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "# Patterns"; then
767
+ pass "load patterns → learned-patterns.md"
768
+ else
769
+ fail_case "load alias" "exit=$EXIT"
770
+ fi
771
+
772
+ # 52. load <missing-file> → "no entries" stub, exit 0
773
+ TMP=$(mktmp)
774
+ mkdir -p "$TMP/.claude/knowledge"
775
+ EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" load fixes 2>&1) || EXIT=$?
776
+ if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "no entries"; then
777
+ pass "load missing → exit 0 with stub (skill-pipeable)"
778
+ else
779
+ fail_case "load missing" "exit=$EXIT"
780
+ fi
781
+
782
+ # 53. append a pattern → entry lands on disk
783
+ TMP=$(mktmp)
784
+ EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" append --type pattern --title "RLS rule" --body "Add RLS in same migration as table" 2>&1) || EXIT=$?
785
+ if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "appended"; then
786
+ pass "append pattern → exit 0 with confirmation"
787
+ else
788
+ fail_case "append pattern" "exit=$EXIT"
789
+ fi
790
+ if [ -f "$TMP/.claude/knowledge/learned-patterns.md" ] \
791
+ && grep -q "### RLS rule" "$TMP/.claude/knowledge/learned-patterns.md" \
792
+ && grep -q "Add RLS in same migration" "$TMP/.claude/knowledge/learned-patterns.md"; then
793
+ pass "appended entry has title + body"
794
+ else
795
+ fail_case "append content"
796
+ fi
797
+
798
+ # 54. append without --title → exit 1
799
+ EXIT=0; HOME="$TMP" $NODE "$KN" append --type pattern --body "x" >/dev/null 2>&1 || EXIT=$?
800
+ if [ "$EXIT" -eq 1 ]; then
801
+ pass "append missing --title → exit 1"
802
+ else
803
+ fail_case "append missing title" "exit=$EXIT"
804
+ fi
805
+
806
+ # 55. append with bad type → exit 1
807
+ EXIT=0; HOME="$TMP" $NODE "$KN" append --type bogus --title T --body B >/dev/null 2>&1 || EXIT=$?
808
+ if [ "$EXIT" -eq 1 ]; then
809
+ pass "append bad --type → exit 1"
810
+ else
811
+ fail_case "append bad type" "exit=$EXIT"
812
+ fi
813
+
814
+ # 56. search finds an appended entry
815
+ TMP=$(mktmp)
816
+ HOME="$TMP" $NODE "$KN" append --type fix --title "Vercel build crash" --body "use node 20.x in package.json engines" >/dev/null 2>&1
817
+ EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" search "Vercel" 2>&1) || EXIT=$?
818
+ if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "Vercel"; then
819
+ pass "search finds appended entries"
820
+ else
821
+ fail_case "search appended" "exit=$EXIT"
822
+ fi
823
+
824
+ # 57. search with no matches → "no matches" stub, exit 0
825
+ EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" search "xyzzy_nonexistent_12345" 2>&1) || EXIT=$?
826
+ if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "no matches"; then
827
+ pass "search no matches → exit 0 with stub"
828
+ else
829
+ fail_case "search no matches" "exit=$EXIT"
830
+ fi
831
+
832
+ # 58. list shows existing files
833
+ TMP=$(mktmp)
834
+ mkdir -p "$TMP/.claude/knowledge"
835
+ echo "x" > "$TMP/.claude/knowledge/foo.md"
836
+ EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" list 2>&1) || EXIT=$?
837
+ if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "foo.md"; then
838
+ pass "list shows existing files"
839
+ else
840
+ fail_case "list" "exit=$EXIT"
841
+ fi
842
+
843
+ # 59. unknown command falls through to load (`knowledge.js patterns` shorthand)
844
+ TMP=$(mktmp)
845
+ mkdir -p "$TMP/.claude/knowledge"
846
+ echo "# Patterns content" > "$TMP/.claude/knowledge/learned-patterns.md"
847
+ EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" patterns 2>&1) || EXIT=$?
848
+ if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "# Patterns content"; then
849
+ pass "unknown command → falls through to load"
850
+ else
851
+ fail_case "fallthrough" "exit=$EXIT"
852
+ fi
853
+
854
+ # 60. path command resolves alias to absolute path (no read)
855
+ EXIT=0; OUT=$($NODE "$KN" path patterns 2>&1) || EXIT=$?
856
+ if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "learned-patterns.md"; then
857
+ pass "path resolves alias to filename"
858
+ else
859
+ fail_case "path" "exit=$EXIT"
860
+ fi
861
+
862
+ echo ""
863
+ echo "--- v4.3.0 (loader subdirs + self-healing skills) ---"
864
+
865
+ # 65. Subdirectory-qualified path: knowledge.js load concepts/foo
866
+ TMP=$(mktmp)
867
+ mkdir -p "$TMP/.claude/knowledge/concepts"
868
+ echo "# Stripe checkout" > "$TMP/.claude/knowledge/concepts/stripe.md"
869
+ EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" load concepts/stripe 2>&1) || EXIT=$?
870
+ if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "Stripe checkout"; then
871
+ pass "load concepts/stripe → reads concepts/stripe.md"
872
+ else
873
+ fail_case "subdir-qualified load" "exit=$EXIT"
874
+ fi
875
+
876
+ # 66. Bare name auto-discovers in concepts/
877
+ TMP=$(mktmp)
878
+ mkdir -p "$TMP/.claude/knowledge/concepts"
879
+ echo "# Voice agent state" > "$TMP/.claude/knowledge/concepts/voice-agent-call-state.md"
880
+ EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" load voice-agent-call-state 2>&1) || EXIT=$?
881
+ if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "Voice agent state"; then
882
+ pass "load <bare> auto-discovers in concepts/"
883
+ else
884
+ fail_case "bare-name subdir discovery" "exit=$EXIT"
885
+ fi
886
+
887
+ # 67. Top-level wins when both top-level and subdir have same filename
888
+ TMP=$(mktmp)
889
+ mkdir -p "$TMP/.claude/knowledge/concepts"
890
+ echo "# Top level" > "$TMP/.claude/knowledge/foo.md"
891
+ echo "# Subdir version" > "$TMP/.claude/knowledge/concepts/foo.md"
892
+ EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" load foo 2>&1) || EXIT=$?
893
+ if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "Top level" && ! echo "$OUT" | grep -q "Subdir version"; then
894
+ pass "top-level wins over subdir on bare-name lookup"
895
+ else
896
+ fail_case "precedence" "exit=$EXIT"
897
+ fi
898
+
899
+ # 68. Subdir-qualified still works when top-level exists with same name
900
+ EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" load concepts/foo 2>&1) || EXIT=$?
901
+ if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "Subdir version"; then
902
+ pass "qualified path overrides top-level"
903
+ else
904
+ fail_case "qualified override" "exit=$EXIT"
905
+ fi
906
+
907
+ # 69. Search recurses into subdirs
908
+ TMP=$(mktmp)
909
+ mkdir -p "$TMP/.claude/knowledge/concepts"
910
+ echo "supabase RLS pattern" > "$TMP/.claude/knowledge/concepts/auth.md"
911
+ EXIT=0; OUT=$(HOME="$TMP" $NODE "$KN" search RLS 2>&1) || EXIT=$?
912
+ if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "concepts/auth.md"; then
913
+ pass "search recurses into concepts/ subdir"
914
+ else
915
+ fail_case "search subdir" "exit=$EXIT"
916
+ fi
917
+
918
+ # 70. qualia-postmortem skill installs
919
+ TMP=$(mktmp)
920
+ echo "QS-FAWZI-01" | HOME="$TMP" $NODE "$INSTALL_JS" >/dev/null 2>&1
921
+ if [ -f "$TMP/.claude/skills/qualia-postmortem/SKILL.md" ]; then
922
+ pass "qualia-postmortem skill installs"
923
+ else
924
+ fail_case "qualia-postmortem skill missing"
925
+ fi
926
+
927
+ # 71. /qualia-verify skill documents --adversarial flag
928
+ if grep -qF -- '--adversarial' "$TMP/.claude/skills/qualia-verify/SKILL.md"; then
929
+ pass "qualia-verify documents --adversarial flag"
930
+ else
931
+ fail_case "qualia-verify missing --adversarial"
932
+ fi
933
+
934
+ # 72. /qualia-verify wires /qualia-postmortem on FAIL
935
+ if grep -q 'qualia-postmortem' "$TMP/.claude/skills/qualia-verify/SKILL.md"; then
936
+ pass "qualia-verify wires /qualia-postmortem on FAIL"
937
+ else
938
+ fail_case "qualia-verify missing postmortem wiring"
939
+ fi
940
+
941
+ # 73. knowledge-flush.js installs at ~/.claude/bin/
942
+ if [ -f "$TMP/.claude/bin/knowledge-flush.js" ]; then
943
+ pass "knowledge-flush.js installs"
944
+ else
945
+ fail_case "knowledge-flush.js missing after install"
946
+ fi
947
+
948
+ # 74. knowledge-flush.js is syntactically valid Node
949
+ EXIT=0; $NODE -c "$TMP/.claude/bin/knowledge-flush.js" 2>/dev/null || EXIT=$?
950
+ if [ "$EXIT" -eq 0 ]; then
951
+ pass "knowledge-flush.js parses as valid Node"
952
+ else
953
+ fail_case "knowledge-flush.js parse error"
954
+ fi
955
+
956
+ # 75. knowledge-flush.js exits 0 with no daily-log (cron-friendly: no spam)
957
+ TMP_HOME=$(mktmp)
958
+ mkdir -p "$TMP_HOME/.claude/knowledge"
959
+ EXIT=0; HOME="$TMP_HOME" $NODE "$FRAMEWORK_DIR/bin/knowledge-flush.js" >/dev/null 2>&1 || EXIT=$?
960
+ if [ "$EXIT" -eq 0 ]; then
961
+ pass "knowledge-flush exits 0 with no daily-log (no cron spam)"
962
+ else
963
+ fail_case "knowledge-flush exited $EXIT with no daily-log"
964
+ fi
965
+
966
+ # 76. knowledge-flush.js writes structured JSONL log even on skip
967
+ TMP_HOME=$(mktmp)
968
+ HOME="$TMP_HOME" $NODE "$FRAMEWORK_DIR/bin/knowledge-flush.js" >/dev/null 2>&1
969
+ if [ -f "$TMP_HOME/.claude/.qualia-flush.log" ] && grep -q '"event":"skipped"' "$TMP_HOME/.claude/.qualia-flush.log"; then
970
+ pass "knowledge-flush writes JSONL audit log"
971
+ else
972
+ fail_case "knowledge-flush no audit log"
973
+ fi
974
+
975
+ # 77. CLI `qualia-framework flush` dispatches to the script (errors when not installed)
976
+ TMP_HOME=$(mktmp)
977
+ EXIT=0; OUT=$(HOME="$TMP_HOME" $NODE "$CLI_JS" flush 2>&1) || EXIT=$?
978
+ if [ "$EXIT" -eq 1 ] && echo "$OUT" | grep -q "knowledge-flush.js not installed"; then
979
+ pass "CLI flush command surfaces install error gracefully"
980
+ else
981
+ fail_case "CLI flush dispatch" "exit=$EXIT out=$OUT"
982
+ fi
983
+
984
+ # 78. CLI flush command appears in help text
985
+ EXIT=0; OUT=$($NODE "$CLI_JS" help 2>&1) || EXIT=$?
986
+ if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "flush"; then
987
+ pass "help text documents flush command"
988
+ else
989
+ fail_case "help missing flush" "exit=$EXIT"
990
+ fi
991
+
685
992
  echo ""
686
993
  echo "=== Results: $PASS passed, $FAIL failed ==="
687
994
  [ "$FAIL" -eq 0 ] && exit 0 || exit 1
@@ -379,6 +379,128 @@ else
379
379
  fi
380
380
  rm -rf "$TMP"
381
381
 
382
+ # --- git-guardrails.js ---
383
+ echo ""
384
+ echo "git-guardrails:"
385
+
386
+ # No stdin / no command → allow (exit 0)
387
+ echo '{}' | $NODE "$HOOKS_DIR/git-guardrails.js" > /dev/null 2>&1
388
+ assert_exit "no command → allowed" 0 $?
389
+
390
+ # Force push to main → BLOCK
391
+ echo '{"tool_input":{"command":"git push --force origin main"}}' | $NODE "$HOOKS_DIR/git-guardrails.js" > /dev/null 2>&1
392
+ assert_exit "force-push to main → blocked" 2 $?
393
+
394
+ echo '{"tool_input":{"command":"git push -f origin master"}}' | $NODE "$HOOKS_DIR/git-guardrails.js" > /dev/null 2>&1
395
+ assert_exit "force-push (-f) to master → blocked" 2 $?
396
+
397
+ # --force-with-lease to main → ALLOW (the safe variant)
398
+ echo '{"tool_input":{"command":"git push --force-with-lease origin main"}}' | $NODE "$HOOKS_DIR/git-guardrails.js" > /dev/null 2>&1
399
+ assert_exit "--force-with-lease to main → allowed" 0 $?
400
+
401
+ # Regular push → ALLOW
402
+ echo '{"tool_input":{"command":"git push origin feature/x"}}' | $NODE "$HOOKS_DIR/git-guardrails.js" > /dev/null 2>&1
403
+ assert_exit "regular push → allowed" 0 $?
404
+
405
+ # git clean -fd → BLOCK
406
+ echo '{"tool_input":{"command":"git clean -fd"}}' | $NODE "$HOOKS_DIR/git-guardrails.js" > /dev/null 2>&1
407
+ assert_exit "git clean -fd → blocked" 2 $?
408
+
409
+ echo '{"tool_input":{"command":"git clean -fdx"}}' | $NODE "$HOOKS_DIR/git-guardrails.js" > /dev/null 2>&1
410
+ assert_exit "git clean -fdx → blocked" 2 $?
411
+
412
+ # git branch -D main → BLOCK
413
+ echo '{"tool_input":{"command":"git branch -D main"}}' | $NODE "$HOOKS_DIR/git-guardrails.js" > /dev/null 2>&1
414
+ assert_exit "git branch -D main → blocked" 2 $?
415
+
416
+ # rm -rf .git → BLOCK
417
+ echo '{"tool_input":{"command":"rm -rf .git"}}' | $NODE "$HOOKS_DIR/git-guardrails.js" > /dev/null 2>&1
418
+ assert_exit "rm -rf .git → blocked" 2 $?
419
+
420
+ # rm -rf src → ALLOW (only .git is special)
421
+ echo '{"tool_input":{"command":"rm -rf src/old"}}' | $NODE "$HOOKS_DIR/git-guardrails.js" > /dev/null 2>&1
422
+ assert_exit "rm -rf src/old → allowed" 0 $?
423
+
424
+ # QUALIA_ALLOW_DESTRUCTIVE=1 escape hatch → ALLOW even force push
425
+ echo '{"tool_input":{"command":"git push --force origin main"}}' | QUALIA_ALLOW_DESTRUCTIVE=1 $NODE "$HOOKS_DIR/git-guardrails.js" > /dev/null 2>&1
426
+ assert_exit "QUALIA_ALLOW_DESTRUCTIVE=1 → allowed despite force-push" 0 $?
427
+
428
+ # Block reason includes "BLOCKED" + "main" for force push to main
429
+ OUT=$(echo '{"tool_input":{"command":"git push --force origin main"}}' | $NODE "$HOOKS_DIR/git-guardrails.js" 2>&1)
430
+ if echo "$OUT" | grep -q "BLOCKED" && echo "$OUT" | grep -q "main"; then
431
+ echo " ✓ block reason includes BLOCKED + main"
432
+ PASS=$((PASS + 1))
433
+ else
434
+ echo " ✗ block reason missing BLOCKED or main: $OUT"
435
+ FAIL=$((FAIL + 1))
436
+ fi
437
+
438
+ # --- stop-session-log.js ---
439
+ echo ""
440
+ echo "stop-session-log:"
441
+
442
+ # Outside a git repo, with nothing to log → exit 0 silent
443
+ TMP=$(mktemp -d)
444
+ HOME="$TMP" $NODE "$HOOKS_DIR/stop-session-log.js" >/dev/null 2>&1
445
+ assert_exit "no activity → exit 0 silent" 0 $?
446
+ # Daily log dir might or might not exist; the hook must NOT crash either way.
447
+ rm -rf "$TMP"
448
+
449
+ # In a git repo with .planning/tracking.json → writes to daily log
450
+ TMP=$(mktemp -d)
451
+ mkdir -p "$TMP/proj/.planning"
452
+ (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)
453
+ echo "x" > "$TMP/proj/file.txt"
454
+ (cd "$TMP/proj" && git add file.txt && git commit -q -m "seed" 2>/dev/null)
455
+ cat > "$TMP/proj/.planning/tracking.json" <<EOF
456
+ {"phase":2,"phase_total":4,"tasks_done":3,"tasks_total":5}
457
+ EOF
458
+ # Touch a file so the diff has something
459
+ echo "y" >> "$TMP/proj/file.txt"
460
+ (cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/stop-session-log.js" >/dev/null 2>&1)
461
+ assert_exit "with activity → exit 0" 0 $?
462
+ TODAY=$(date -u +%Y-%m-%d)
463
+ LOG_FILE="$TMP/.claude/knowledge/daily-log/$TODAY.md"
464
+ if [ -f "$LOG_FILE" ] && grep -q "phase=2/4" "$LOG_FILE" && grep -q "tasks=3/5" "$LOG_FILE"; then
465
+ echo " ✓ daily-log contains phase + tasks"
466
+ PASS=$((PASS + 1))
467
+ else
468
+ echo " ✗ daily-log missing or malformed: $(cat "$LOG_FILE" 2>/dev/null)"
469
+ FAIL=$((FAIL + 1))
470
+ fi
471
+ # Header is project name (basename of repo root)
472
+ if [ -f "$LOG_FILE" ] && grep -q "^## proj$" "$LOG_FILE"; then
473
+ echo " ✓ daily-log has project header"
474
+ PASS=$((PASS + 1))
475
+ else
476
+ echo " ✗ daily-log missing project header"
477
+ FAIL=$((FAIL + 1))
478
+ fi
479
+ rm -rf "$TMP"
480
+
481
+ # Stop hook is idempotent within MIN_INTERVAL_MS — second run within 5min skips
482
+ TMP=$(mktemp -d)
483
+ mkdir -p "$TMP/proj/.planning" "$TMP/.claude"
484
+ (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)
485
+ echo "z" > "$TMP/proj/f.txt"
486
+ (cd "$TMP/proj" && git add f.txt && git commit -q -m "s" 2>/dev/null)
487
+ echo '{"phase":1,"phase_total":2,"tasks_done":1,"tasks_total":2}' > "$TMP/proj/.planning/tracking.json"
488
+ echo "a" >> "$TMP/proj/f.txt"
489
+ # First run — writes
490
+ (cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/stop-session-log.js" >/dev/null 2>&1)
491
+ LINES_BEFORE=$(wc -l < "$TMP/.claude/knowledge/daily-log/$(date -u +%Y-%m-%d).md" 2>/dev/null || echo 0)
492
+ # Second run within 5min — must skip
493
+ (cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/stop-session-log.js" >/dev/null 2>&1)
494
+ LINES_AFTER=$(wc -l < "$TMP/.claude/knowledge/daily-log/$(date -u +%Y-%m-%d).md" 2>/dev/null || echo 0)
495
+ if [ "$LINES_BEFORE" = "$LINES_AFTER" ]; then
496
+ echo " ✓ second run within MIN_INTERVAL skips (no double-write)"
497
+ PASS=$((PASS + 1))
498
+ else
499
+ echo " ✗ second run wrote anyway ($LINES_BEFORE → $LINES_AFTER)"
500
+ FAIL=$((FAIL + 1))
501
+ fi
502
+ rm -rf "$TMP"
503
+
382
504
  echo ""
383
505
  echo "=== Results: $PASS passed, $FAIL failed ==="
384
506
  [ "$FAIL" -eq 0 ] && exit 0 || exit 1
package/tests/runner.js CHANGED
@@ -2536,7 +2536,12 @@ describe("install.js", () => {
2536
2536
  assert.ok(fs.existsSync(path.join(tmpHome, ".claude", "bin", "state.js")));
2537
2537
  assert.ok(fs.existsSync(path.join(tmpHome, ".claude", "bin", "qualia-ui.js")));
2538
2538
  assert.ok(fs.existsSync(path.join(tmpHome, ".claude", "bin", "statusline.js")));
2539
+ assert.ok(fs.existsSync(path.join(tmpHome, ".claude", "bin", "knowledge.js")));
2539
2540
  assert.ok(fs.existsSync(path.join(tmpHome, ".claude", ".qualia-config.json")));
2541
+ // v4.2.0 — knowledge layer must be initialized
2542
+ assert.ok(fs.existsSync(path.join(tmpHome, ".claude", "knowledge", "agents.md")));
2543
+ assert.ok(fs.existsSync(path.join(tmpHome, ".claude", "knowledge", "index.md")));
2544
+ assert.ok(fs.existsSync(path.join(tmpHome, ".claude", "knowledge", "daily-log")));
2540
2545
  } finally {
2541
2546
  fs.rmSync(tmpHome, { recursive: true, force: true });
2542
2547
  }
@@ -2567,12 +2572,12 @@ describe("install.js", () => {
2567
2572
  }
2568
2573
  });
2569
2574
 
2570
- it("7 hooks installed (block-env-edit removed in v3.2.0)", () => {
2575
+ it("9 hooks installed (block-env-edit removed in v3.2.0; git-guardrails + stop-session-log added in v4.2.0)", () => {
2571
2576
  const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-install-"));
2572
2577
  try {
2573
2578
  runInstall("QS-FAWZI-01", tmpHome);
2574
2579
  const hooks = fs.readdirSync(path.join(tmpHome, ".claude", "hooks")).filter(f => f.endsWith(".js"));
2575
- assert.equal(hooks.length, 7);
2580
+ assert.equal(hooks.length, 9);
2576
2581
  } finally {
2577
2582
  fs.rmSync(tmpHome, { recursive: true, force: true });
2578
2583
  }