gitmem-mcp 0.2.0 → 1.0.1

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 (249) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/CLAUDE.md.template +63 -55
  3. package/README.md +149 -120
  4. package/bin/gitmem.js +377 -25
  5. package/bin/init-wizard.js +642 -0
  6. package/bin/uninstall.js +288 -0
  7. package/dist/commands/check.js +20 -20
  8. package/dist/commands/check.js.map +1 -1
  9. package/dist/constants/closing-questions.d.ts +6 -0
  10. package/dist/constants/closing-questions.d.ts.map +1 -1
  11. package/dist/constants/closing-questions.js +65 -0
  12. package/dist/constants/closing-questions.js.map +1 -1
  13. package/dist/hooks/format-utils.d.ts +52 -0
  14. package/dist/hooks/format-utils.d.ts.map +1 -0
  15. package/dist/hooks/format-utils.js +89 -0
  16. package/dist/hooks/format-utils.js.map +1 -0
  17. package/dist/hooks/quick-retrieve.d.ts +30 -0
  18. package/dist/hooks/quick-retrieve.d.ts.map +1 -0
  19. package/dist/hooks/quick-retrieve.js +149 -0
  20. package/dist/hooks/quick-retrieve.js.map +1 -0
  21. package/dist/index.js +0 -0
  22. package/dist/schemas/active-sessions.d.ts +8 -8
  23. package/dist/schemas/analyze.d.ts +3 -3
  24. package/dist/schemas/common.d.ts +2 -2
  25. package/dist/schemas/common.d.ts.map +1 -1
  26. package/dist/schemas/common.js +1 -1
  27. package/dist/schemas/common.js.map +1 -1
  28. package/dist/schemas/create-decision.d.ts +3 -3
  29. package/dist/schemas/create-learning.d.ts +13 -13
  30. package/dist/schemas/log.d.ts +3 -3
  31. package/dist/schemas/prepare-context.d.ts +3 -3
  32. package/dist/schemas/recall.d.ts +3 -3
  33. package/dist/schemas/record-scar-usage-batch.d.ts +8 -3
  34. package/dist/schemas/record-scar-usage-batch.d.ts.map +1 -1
  35. package/dist/schemas/record-scar-usage.d.ts +3 -0
  36. package/dist/schemas/record-scar-usage.d.ts.map +1 -1
  37. package/dist/schemas/record-scar-usage.js +1 -0
  38. package/dist/schemas/record-scar-usage.js.map +1 -1
  39. package/dist/schemas/registry.d.ts +18 -0
  40. package/dist/schemas/registry.d.ts.map +1 -0
  41. package/dist/schemas/registry.js +158 -0
  42. package/dist/schemas/registry.js.map +1 -0
  43. package/dist/schemas/save-transcript.d.ts +3 -3
  44. package/dist/schemas/search-transcripts.d.ts +33 -0
  45. package/dist/schemas/search-transcripts.d.ts.map +1 -0
  46. package/dist/schemas/search-transcripts.js +26 -0
  47. package/dist/schemas/search-transcripts.js.map +1 -0
  48. package/dist/schemas/search.d.ts +3 -3
  49. package/dist/schemas/session-close.d.ts +43 -15
  50. package/dist/schemas/session-close.d.ts.map +1 -1
  51. package/dist/schemas/session-close.js +7 -2
  52. package/dist/schemas/session-close.js.map +1 -1
  53. package/dist/schemas/session-start.d.ts +3 -3
  54. package/dist/schemas/thread.d.ts +3 -3
  55. package/dist/server.d.ts.map +1 -1
  56. package/dist/server.js +82 -28
  57. package/dist/server.js.map +1 -1
  58. package/dist/services/active-sessions.d.ts +2 -1
  59. package/dist/services/active-sessions.d.ts.map +1 -1
  60. package/dist/services/active-sessions.js +130 -84
  61. package/dist/services/active-sessions.js.map +1 -1
  62. package/dist/services/analytics.d.ts.map +1 -1
  63. package/dist/services/analytics.js +1 -0
  64. package/dist/services/analytics.js.map +1 -1
  65. package/dist/services/behavioral-decay.d.ts +40 -0
  66. package/dist/services/behavioral-decay.d.ts.map +1 -0
  67. package/dist/services/behavioral-decay.js +110 -0
  68. package/dist/services/behavioral-decay.js.map +1 -0
  69. package/dist/services/bm25.d.ts +39 -0
  70. package/dist/services/bm25.d.ts.map +1 -0
  71. package/dist/services/bm25.js +132 -0
  72. package/dist/services/bm25.js.map +1 -0
  73. package/dist/services/cache.d.ts.map +1 -1
  74. package/dist/services/cache.js +9 -8
  75. package/dist/services/cache.js.map +1 -1
  76. package/dist/services/cache.test.js +17 -17
  77. package/dist/services/cache.test.js.map +1 -1
  78. package/dist/services/compliance-validator.d.ts.map +1 -1
  79. package/dist/services/compliance-validator.js +12 -1
  80. package/dist/services/compliance-validator.js.map +1 -1
  81. package/dist/services/display-protocol.d.ts +31 -0
  82. package/dist/services/display-protocol.d.ts.map +1 -0
  83. package/dist/services/display-protocol.js +73 -0
  84. package/dist/services/display-protocol.js.map +1 -0
  85. package/dist/services/effect-tracker.d.ts +81 -0
  86. package/dist/services/effect-tracker.d.ts.map +1 -0
  87. package/dist/services/effect-tracker.js +181 -0
  88. package/dist/services/effect-tracker.js.map +1 -0
  89. package/dist/services/file-lock.d.ts +31 -0
  90. package/dist/services/file-lock.d.ts.map +1 -0
  91. package/dist/services/file-lock.js +124 -0
  92. package/dist/services/file-lock.js.map +1 -0
  93. package/dist/services/gitmem-dir.d.ts +7 -0
  94. package/dist/services/gitmem-dir.d.ts.map +1 -1
  95. package/dist/services/gitmem-dir.js +21 -0
  96. package/dist/services/gitmem-dir.js.map +1 -1
  97. package/dist/services/local-file-storage.d.ts +3 -2
  98. package/dist/services/local-file-storage.d.ts.map +1 -1
  99. package/dist/services/local-file-storage.js +30 -43
  100. package/dist/services/local-file-storage.js.map +1 -1
  101. package/dist/services/local-vector-search.d.ts +10 -9
  102. package/dist/services/local-vector-search.d.ts.map +1 -1
  103. package/dist/services/local-vector-search.js +28 -23
  104. package/dist/services/local-vector-search.js.map +1 -1
  105. package/dist/services/metrics.d.ts +7 -2
  106. package/dist/services/metrics.d.ts.map +1 -1
  107. package/dist/services/metrics.js +41 -33
  108. package/dist/services/metrics.js.map +1 -1
  109. package/dist/services/session-state.d.ts +8 -0
  110. package/dist/services/session-state.d.ts.map +1 -1
  111. package/dist/services/session-state.js +9 -2
  112. package/dist/services/session-state.js.map +1 -1
  113. package/dist/services/startup.d.ts +12 -13
  114. package/dist/services/startup.d.ts.map +1 -1
  115. package/dist/services/startup.js +104 -57
  116. package/dist/services/startup.js.map +1 -1
  117. package/dist/services/supabase-client.d.ts +2 -1
  118. package/dist/services/supabase-client.d.ts.map +1 -1
  119. package/dist/services/supabase-client.js +22 -16
  120. package/dist/services/supabase-client.js.map +1 -1
  121. package/dist/services/thread-dedup.d.ts +9 -0
  122. package/dist/services/thread-dedup.d.ts.map +1 -1
  123. package/dist/services/thread-dedup.js +27 -0
  124. package/dist/services/thread-dedup.js.map +1 -1
  125. package/dist/services/thread-manager.d.ts.map +1 -1
  126. package/dist/services/thread-manager.js +38 -16
  127. package/dist/services/thread-manager.js.map +1 -1
  128. package/dist/services/thread-suggestions.d.ts.map +1 -1
  129. package/dist/services/thread-suggestions.js +1 -1
  130. package/dist/services/thread-suggestions.js.map +1 -1
  131. package/dist/services/thread-supabase.d.ts +0 -1
  132. package/dist/services/thread-supabase.d.ts.map +1 -1
  133. package/dist/services/thread-supabase.js +83 -54
  134. package/dist/services/thread-supabase.js.map +1 -1
  135. package/dist/services/timezone.d.ts.map +1 -1
  136. package/dist/services/timezone.js +1 -0
  137. package/dist/services/timezone.js.map +1 -1
  138. package/dist/services/transcript-chunker.d.ts.map +1 -1
  139. package/dist/services/transcript-chunker.js +18 -4
  140. package/dist/services/transcript-chunker.js.map +1 -1
  141. package/dist/services/variant-generation.d.ts +41 -0
  142. package/dist/services/variant-generation.d.ts.map +1 -0
  143. package/dist/services/variant-generation.js +263 -0
  144. package/dist/services/variant-generation.js.map +1 -0
  145. package/dist/tools/absorb-observations.d.ts.map +1 -1
  146. package/dist/tools/absorb-observations.js +9 -0
  147. package/dist/tools/absorb-observations.js.map +1 -1
  148. package/dist/tools/analyze.d.ts.map +1 -1
  149. package/dist/tools/analyze.js +13 -2
  150. package/dist/tools/analyze.js.map +1 -1
  151. package/dist/tools/archive-learning.d.ts +28 -0
  152. package/dist/tools/archive-learning.d.ts.map +1 -0
  153. package/dist/tools/archive-learning.js +81 -0
  154. package/dist/tools/archive-learning.js.map +1 -0
  155. package/dist/tools/cleanup-threads.d.ts +1 -0
  156. package/dist/tools/cleanup-threads.d.ts.map +1 -1
  157. package/dist/tools/cleanup-threads.js +111 -18
  158. package/dist/tools/cleanup-threads.js.map +1 -1
  159. package/dist/tools/confirm-scars.d.ts.map +1 -1
  160. package/dist/tools/confirm-scars.js +8 -2
  161. package/dist/tools/confirm-scars.js.map +1 -1
  162. package/dist/tools/create-decision.d.ts.map +1 -1
  163. package/dist/tools/create-decision.js +11 -8
  164. package/dist/tools/create-decision.js.map +1 -1
  165. package/dist/tools/create-learning.d.ts.map +1 -1
  166. package/dist/tools/create-learning.js +35 -11
  167. package/dist/tools/create-learning.js.map +1 -1
  168. package/dist/tools/create-linear-issue.d.ts +18 -0
  169. package/dist/tools/create-linear-issue.d.ts.map +1 -0
  170. package/dist/tools/create-linear-issue.js +197 -0
  171. package/dist/tools/create-linear-issue.js.map +1 -0
  172. package/dist/tools/create-thread.d.ts +2 -1
  173. package/dist/tools/create-thread.d.ts.map +1 -1
  174. package/dist/tools/create-thread.js +9 -4
  175. package/dist/tools/create-thread.js.map +1 -1
  176. package/dist/tools/definitions.d.ts +785 -34
  177. package/dist/tools/definitions.d.ts.map +1 -1
  178. package/dist/tools/definitions.js +239 -95
  179. package/dist/tools/definitions.js.map +1 -1
  180. package/dist/tools/dismiss-suggestion.d.ts +1 -0
  181. package/dist/tools/dismiss-suggestion.d.ts.map +1 -1
  182. package/dist/tools/dismiss-suggestion.js +4 -0
  183. package/dist/tools/dismiss-suggestion.js.map +1 -1
  184. package/dist/tools/graph-traverse.d.ts +1 -0
  185. package/dist/tools/graph-traverse.d.ts.map +1 -1
  186. package/dist/tools/graph-traverse.js +24 -9
  187. package/dist/tools/graph-traverse.js.map +1 -1
  188. package/dist/tools/list-threads.d.ts.map +1 -1
  189. package/dist/tools/list-threads.js +49 -5
  190. package/dist/tools/list-threads.js.map +1 -1
  191. package/dist/tools/log.d.ts +1 -0
  192. package/dist/tools/log.d.ts.map +1 -1
  193. package/dist/tools/log.js +84 -17
  194. package/dist/tools/log.js.map +1 -1
  195. package/dist/tools/prepare-context.d.ts +1 -0
  196. package/dist/tools/prepare-context.d.ts.map +1 -1
  197. package/dist/tools/prepare-context.js +15 -85
  198. package/dist/tools/prepare-context.js.map +1 -1
  199. package/dist/tools/promote-suggestion.d.ts +1 -0
  200. package/dist/tools/promote-suggestion.d.ts.map +1 -1
  201. package/dist/tools/promote-suggestion.js +5 -0
  202. package/dist/tools/promote-suggestion.js.map +1 -1
  203. package/dist/tools/recall.d.ts +2 -0
  204. package/dist/tools/recall.d.ts.map +1 -1
  205. package/dist/tools/recall.js +43 -10
  206. package/dist/tools/recall.js.map +1 -1
  207. package/dist/tools/recall.test.js +6 -6
  208. package/dist/tools/recall.test.js.map +1 -1
  209. package/dist/tools/record-scar-usage-batch.d.ts.map +1 -1
  210. package/dist/tools/record-scar-usage-batch.js +13 -0
  211. package/dist/tools/record-scar-usage-batch.js.map +1 -1
  212. package/dist/tools/record-scar-usage.d.ts.map +1 -1
  213. package/dist/tools/record-scar-usage.js +6 -0
  214. package/dist/tools/record-scar-usage.js.map +1 -1
  215. package/dist/tools/resolve-thread.d.ts.map +1 -1
  216. package/dist/tools/resolve-thread.js +57 -6
  217. package/dist/tools/resolve-thread.js.map +1 -1
  218. package/dist/tools/save-transcript.d.ts +1 -0
  219. package/dist/tools/save-transcript.d.ts.map +1 -1
  220. package/dist/tools/save-transcript.js +3 -1
  221. package/dist/tools/save-transcript.js.map +1 -1
  222. package/dist/tools/search-transcripts.d.ts +44 -0
  223. package/dist/tools/search-transcripts.d.ts.map +1 -0
  224. package/dist/tools/search-transcripts.js +158 -0
  225. package/dist/tools/search-transcripts.js.map +1 -0
  226. package/dist/tools/search.d.ts +1 -0
  227. package/dist/tools/search.d.ts.map +1 -1
  228. package/dist/tools/search.js +74 -3
  229. package/dist/tools/search.js.map +1 -1
  230. package/dist/tools/session-close.d.ts.map +1 -1
  231. package/dist/tools/session-close.js +563 -326
  232. package/dist/tools/session-close.js.map +1 -1
  233. package/dist/tools/session-start.d.ts +10 -6
  234. package/dist/tools/session-start.d.ts.map +1 -1
  235. package/dist/tools/session-start.js +317 -426
  236. package/dist/tools/session-start.js.map +1 -1
  237. package/dist/types/index.d.ts +37 -4
  238. package/dist/types/index.d.ts.map +1 -1
  239. package/hooks/.claude-plugin/plugin.json +8 -0
  240. package/hooks/README.md +107 -0
  241. package/hooks/hooks/hooks.json +123 -0
  242. package/hooks/scripts/auto-retrieve-hook.sh +163 -0
  243. package/hooks/scripts/post-tool-use.sh +112 -0
  244. package/hooks/scripts/recall-check.sh +213 -0
  245. package/hooks/scripts/session-close-check.sh +116 -0
  246. package/hooks/scripts/session-start.sh +233 -0
  247. package/hooks/tests/test-hooks.sh +577 -0
  248. package/package.json +4 -2
  249. package/schema/setup.sql +1 -1
@@ -0,0 +1,163 @@
1
+ #!/bin/bash
2
+ # GitMem Hooks Plugin — UserPromptSubmit Hook (Auto-Retrieve)
3
+ #
4
+ # Automatically searches institutional memory and injects relevant scars
5
+ # into the agent's context before it starts working.
6
+ #
7
+ # Pipeline:
8
+ # 1. Read user prompt from stdin (hook input JSON)
9
+ # 2. Classify task type via keyword matching (<50ms)
10
+ # 3. If retrieval needed, call quick-retrieve.js (Node module)
11
+ # 4. Inject results as additionalContext
12
+ #
13
+ # Task Types (priority order):
14
+ # trivial → none (confirmations, slash cmds, short prompts)
15
+ # architecture → full (design, decisions, trade-offs)
16
+ # research → full (synthesis, comparison, analysis)
17
+ # content → full (writing, documentation)
18
+ # implementation → scars (building, fixing, issue work)
19
+ # default → scars (conservative default)
20
+ #
21
+ # Design: fail open — if anything breaks, exit 0 (never block the user).
22
+ #
23
+ # Disable: GITMEM_AUTO_RETRIEVE=false
24
+
25
+ set -e
26
+
27
+ # Check if disabled
28
+ if [ "$GITMEM_AUTO_RETRIEVE" = "false" ] || [ "$GITMEM_AUTO_RETRIEVE" = "0" ]; then
29
+ exit 0
30
+ fi
31
+
32
+ # Read hook input from stdin
33
+ HOOK_INPUT=$(cat -)
34
+
35
+ # ============================================================================
36
+ # Extract prompt from hook input
37
+ # ============================================================================
38
+
39
+ # Use node if available, fallback to basic extraction
40
+ PROMPT=""
41
+ if command -v node &>/dev/null; then
42
+ PROMPT=$(echo "$HOOK_INPUT" | node -e "
43
+ let d='';
44
+ process.stdin.on('data',c=>d+=c);
45
+ process.stdin.on('end',()=>{
46
+ try {
47
+ const j=JSON.parse(d);
48
+ process.stdout.write(j.prompt||'');
49
+ } catch(e) { }
50
+ });
51
+ " 2>/dev/null) || true
52
+ fi
53
+
54
+ # Empty or missing prompt → nothing to do
55
+ if [ -z "$PROMPT" ]; then
56
+ exit 0
57
+ fi
58
+
59
+ PROMPT_LOWER=$(echo "$PROMPT" | tr '[:upper:]' '[:lower:]')
60
+ PROMPT_LEN=${#PROMPT}
61
+
62
+ # ============================================================================
63
+ # Task Classification (keyword matching, <50ms)
64
+ # ============================================================================
65
+
66
+ RETRIEVAL_LEVEL=""
67
+
68
+ # Priority 1: Trivial — skip retrieval entirely
69
+ if echo "$PROMPT_LOWER" | grep -qE '^(yes|no|ok|k|y|n|sure|thanks|thank you|continue|go ahead|proceed|correct|right|exactly|got it|sounds good|lgtm|looks good|done|nope|yep|yup|agreed)$'; then
70
+ exit 0
71
+ elif echo "$PROMPT_LOWER" | grep -qE '^/'; then
72
+ exit 0
73
+ elif echo "$PROMPT_LOWER" | grep -qE '^(closing|done for now|wrapping up|wrap up|that.s it|that.s all|gitmem)'; then
74
+ exit 0
75
+ elif echo "$PROMPT_LOWER" | grep -qE '\b(fix\s+typo|typo\b|formatting\b|indent(ation)?\b|whitespace\b|lint(ing)?\b)'; then
76
+ exit 0
77
+ elif [ "$PROMPT_LEN" -lt 12 ]; then
78
+ exit 0
79
+ fi
80
+
81
+ # Priority 2: Architecture → full retrieval
82
+ if echo "$PROMPT_LOWER" | grep -qE '\b(architect(ure)?|design\s+(the\s+)?\w+|decide\s+(between|on|whether|how|if)|should\s+we|trade-?offs?\b|rfc\b|adr\b|system\s+design)\b'; then
83
+ RETRIEVAL_LEVEL="full"
84
+
85
+ # Priority 3: Research → full retrieval
86
+ elif echo "$PROMPT_LOWER" | grep -qE '\b(research\b|synthesize|synthesis\b|compare\s+(and|the|our)|contrast\b|analyze\s+(the\s+)?(options|approaches|alternatives)|evaluate\s+(frameworks|tools|approaches))\b'; then
87
+ RETRIEVAL_LEVEL="full"
88
+
89
+ # Priority 4: Content creation → full retrieval
90
+ elif echo "$PROMPT_LOWER" | grep -qE '\b(write\s+(a|an|the|about)|article\b|blog\s*(post)?|document\s+(the|how|what)|explain\s+(how|what|the)|guide\s+(to|for)|tutorial\b)\b'; then
91
+ RETRIEVAL_LEVEL="full"
92
+
93
+ # Priority 5: Implementation → scars only
94
+ elif echo "$PROMPT_LOWER" | grep -qE '\b(implement|build\b|fix\s+(the\s+)?bug|refactor|migrate\b|pickup\b|pick\s*up|create\s+(a|the)|add\s+(a|the))\b'; then
95
+ RETRIEVAL_LEVEL="scars"
96
+
97
+ # Default: scars (conservative — better to surface something than nothing)
98
+ else
99
+ RETRIEVAL_LEVEL="scars"
100
+ fi
101
+
102
+ # ============================================================================
103
+ # Invoke quick-retrieve (Node module)
104
+ # ============================================================================
105
+
106
+ # Require node for retrieval
107
+ if ! command -v node &>/dev/null; then
108
+ exit 0
109
+ fi
110
+
111
+ # Locate quick-retrieve.js relative to plugin root
112
+ # Hook scripts are at: ${CLAUDE_PLUGIN_ROOT}/scripts/
113
+ # Built JS is at: ${CLAUDE_PLUGIN_ROOT}/../dist/hooks/
114
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
115
+ QUICK_RETRIEVE="${SCRIPT_DIR}/../../dist/hooks/quick-retrieve.js"
116
+
117
+ # Fallback: try relative to CLAUDE_PLUGIN_ROOT
118
+ if [ ! -f "$QUICK_RETRIEVE" ]; then
119
+ QUICK_RETRIEVE="${CLAUDE_PLUGIN_ROOT}/../dist/hooks/quick-retrieve.js"
120
+ fi
121
+
122
+ # If still not found, try the gitmem package location
123
+ if [ ! -f "$QUICK_RETRIEVE" ]; then
124
+ # Check common install locations
125
+ for CANDIDATE in \
126
+ "/workspace/gitmem/dist/hooks/quick-retrieve.js" \
127
+ "$(npm root -g 2>/dev/null)/gitmem/dist/hooks/quick-retrieve.js" \
128
+ "$(dirname "$(which gitmem 2>/dev/null)")/../lib/node_modules/gitmem/dist/hooks/quick-retrieve.js"; do
129
+ if [ -f "$CANDIDATE" ]; then
130
+ QUICK_RETRIEVE="$CANDIDATE"
131
+ break
132
+ fi
133
+ done
134
+ fi
135
+
136
+ if [ ! -f "$QUICK_RETRIEVE" ]; then
137
+ # Can't find quick-retrieve module — fail open
138
+ exit 0
139
+ fi
140
+
141
+ # Call quick-retrieve with prompt and level
142
+ # Timeout: 2.5s (leave 500ms buffer within 3s hook timeout)
143
+ RESULT=$(timeout 2.5 node "$QUICK_RETRIEVE" "$PROMPT" "$RETRIEVAL_LEVEL" 2>/dev/null) || true
144
+
145
+ # Empty result → nothing relevant found
146
+ if [ -z "$RESULT" ]; then
147
+ exit 0
148
+ fi
149
+
150
+ # ============================================================================
151
+ # Inject as additionalContext
152
+ # ============================================================================
153
+
154
+ # Escape the result for JSON embedding
155
+ ESCAPED_RESULT=$(node -e "process.stdout.write(JSON.stringify(process.argv[1]))" "$RESULT" 2>/dev/null) || exit 0
156
+
157
+ cat <<HOOKJSON
158
+ {
159
+ "additionalContext": ${ESCAPED_RESULT}
160
+ }
161
+ HOOKJSON
162
+
163
+ exit 0
@@ -0,0 +1,112 @@
1
+ #!/bin/bash
2
+ # GitMem Hooks Plugin — PostToolUse Hook (Audit Trail)
3
+ # Logs LOOKED events (after recall/search) and ACTION events (after consequential actions).
4
+ #
5
+ # Event types:
6
+ # LOOKED — Agent checked institutional memory (recall, search)
7
+ # ACTION — Agent took a consequential action (git push, Linear Done, SQL migration)
8
+ #
9
+ # Audit trail: append-only JSONL at /tmp/gitmem-hooks-{SESSION_ID}/audit.jsonl
10
+ # Non-consequential actions pass through silently.
11
+ #
12
+ # Input: JSON via stdin with tool_name, tool_input, tool_output
13
+ # Output: empty (exit 0) — PostToolUse hooks never block
14
+
15
+ set -e
16
+
17
+ # Read hook input from stdin
18
+ HOOK_INPUT=$(cat -)
19
+
20
+ # ============================================================================
21
+ # Graceful degradation: skip if no active gitmem session
22
+ # ============================================================================
23
+
24
+ # Check active sessions registry (Phase 1 multi-session, GIT-19)
25
+ ACTIVE_SESSIONS=".gitmem/active-sessions.json"
26
+ if [ ! -f "$ACTIVE_SESSIONS" ]; then
27
+ exit 0
28
+ fi
29
+
30
+ # ============================================================================
31
+ # JSON parsing helper (jq preferred, node fallback)
32
+ # ============================================================================
33
+
34
+ parse_json() {
35
+ local INPUT="$1"
36
+ local FIELD="$2"
37
+ if command -v jq &>/dev/null; then
38
+ echo "$INPUT" | jq -r "$FIELD // empty" 2>/dev/null
39
+ else
40
+ echo "$INPUT" | node -e "
41
+ let d='';
42
+ process.stdin.on('data',c=>d+=c);
43
+ process.stdin.on('end',()=>{
44
+ try {
45
+ const j=JSON.parse(d);
46
+ const path='$FIELD'.replace(/^\./,'').split('.');
47
+ let v=j;
48
+ for(const p of path) v=v?.[p];
49
+ process.stdout.write(String(v||''));
50
+ } catch(e) { process.stdout.write(''); }
51
+ });
52
+ " 2>/dev/null
53
+ fi
54
+ }
55
+
56
+ TOOL_NAME=$(parse_json "$HOOK_INPUT" ".tool_name")
57
+
58
+ # ============================================================================
59
+ # Classify event: LOOKED or ACTION
60
+ # ============================================================================
61
+
62
+ EVENT_TYPE=""
63
+ DETAIL=""
64
+
65
+ case "$TOOL_NAME" in
66
+ mcp__gitmem__recall|mcp__gitmem__gitmem-r)
67
+ EVENT_TYPE="LOOKED"
68
+ PLAN=$(parse_json "$HOOK_INPUT" ".tool_input.plan")
69
+ DETAIL="plan: ${PLAN:-<no plan>}"
70
+ ;;
71
+ mcp__gitmem__search|mcp__gitmem__gm-scar)
72
+ EVENT_TYPE="LOOKED"
73
+ QUERY=$(parse_json "$HOOK_INPUT" ".tool_input.query")
74
+ DETAIL="query: ${QUERY:-<no query>}"
75
+ ;;
76
+ Bash)
77
+ COMMAND=$(parse_json "$HOOK_INPUT" ".tool_input.command")
78
+ if echo "$COMMAND" | grep -qE '(git\s+push|git\s+tag|npm\s+publish|npx\s+supabase\s+db\s+push|deploy|supabase\s+functions\s+deploy)'; then
79
+ EVENT_TYPE="ACTION"
80
+ DETAIL="command: $COMMAND"
81
+ fi
82
+ ;;
83
+ Write|Edit)
84
+ FILE_PATH=$(parse_json "$HOOK_INPUT" ".tool_input.file_path")
85
+ if echo "$FILE_PATH" | grep -qE '\.(sql|env)$'; then
86
+ EVENT_TYPE="ACTION"
87
+ DETAIL="file: $FILE_PATH"
88
+ fi
89
+ ;;
90
+ esac
91
+
92
+ # Not a tracked event → pass through silently
93
+ if [ -z "$EVENT_TYPE" ]; then
94
+ exit 0
95
+ fi
96
+
97
+ # ============================================================================
98
+ # Append to audit trail (JSONL)
99
+ # ============================================================================
100
+
101
+ SESSION_ID="${CLAUDE_SESSION_ID:-$$}"
102
+ STATE_DIR="/tmp/gitmem-hooks-${SESSION_ID}"
103
+ mkdir -p "$STATE_DIR"
104
+
105
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
106
+
107
+ # Escape detail for JSON (replace quotes and backslashes)
108
+ DETAIL_ESCAPED=$(echo "$DETAIL" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ')
109
+
110
+ echo "{\"timestamp\":\"${TIMESTAMP}\",\"type\":\"${EVENT_TYPE}\",\"tool\":\"${TOOL_NAME}\",\"detail\":\"${DETAIL_ESCAPED}\"}" >> "$STATE_DIR/audit.jsonl"
111
+
112
+ exit 0
@@ -0,0 +1,213 @@
1
+ #!/bin/bash
2
+ # GitMem Hooks Plugin — PreToolUse Hook (Recall Check + Confirmation Gate)
3
+ #
4
+ # Two enforcement mechanisms for consequential actions:
5
+ #
6
+ # 1. CONFIRMATION GATE (hard block):
7
+ # If recall() surfaced scars but confirm_scars() hasn't been called → BLOCK.
8
+ # Uses JSON "decision: block" pattern (same as session-close-check.sh).
9
+ # Only blocks on recall-source scars; session_start scars don't require confirmation.
10
+ #
11
+ # 2. RECALL NAG (soft reminder):
12
+ # If recall hasn't been called recently → nudge (additionalContext, never blocks).
13
+ # - If recall never called AND >3 tool calls → nag
14
+ # - Cooldown: no more than once per 60 seconds
15
+ #
16
+ # Filter layer: Only triggers on consequential actions:
17
+ # - Bash: git push, git tag, npm publish, deploy commands
18
+ # - Write/Edit: .sql migrations, .env files
19
+ #
20
+ # Input: JSON via stdin with tool_name and tool_input
21
+ # Output: JSON with decision:block OR additionalContext OR empty (exit 0)
22
+
23
+ set -e
24
+
25
+ # Read hook input from stdin
26
+ HOOK_INPUT=$(cat -)
27
+
28
+ # ============================================================================
29
+ # Resolve active session from registry (Phase 1 multi-session, GIT-19)
30
+ # ============================================================================
31
+
32
+ ACTIVE_SESSIONS=".gitmem/active-sessions.json"
33
+ if [ ! -f "$ACTIVE_SESSIONS" ]; then
34
+ exit 0
35
+ fi
36
+
37
+ # Get first session ID from registry to find per-session data file
38
+ SESSION_FILE=""
39
+ if command -v jq &>/dev/null; then
40
+ SID=$(jq -r '.sessions[0].session_id // empty' "$ACTIVE_SESSIONS" 2>/dev/null)
41
+ [ -n "$SID" ] && SESSION_FILE=".gitmem/sessions/${SID}/session.json"
42
+ elif command -v node &>/dev/null; then
43
+ SID=$(node -e "const fs=require('fs');try{const r=JSON.parse(fs.readFileSync('$ACTIVE_SESSIONS','utf8'));const s=(r.sessions||[])[0];process.stdout.write(s?.session_id||'')}catch(e){}" 2>/dev/null)
44
+ [ -n "$SID" ] && SESSION_FILE=".gitmem/sessions/${SID}/session.json"
45
+ fi
46
+
47
+ if [ -z "$SESSION_FILE" ] || [ ! -f "$SESSION_FILE" ]; then
48
+ exit 0
49
+ fi
50
+
51
+ # ============================================================================
52
+ # Extract tool info using jq (node fallback)
53
+ # ============================================================================
54
+
55
+ parse_json() {
56
+ local INPUT="$1"
57
+ local FIELD="$2"
58
+ if command -v jq &>/dev/null; then
59
+ echo "$INPUT" | jq -r "$FIELD // empty" 2>/dev/null
60
+ else
61
+ echo "$INPUT" | node -e "
62
+ let d='';
63
+ process.stdin.on('data',c=>d+=c);
64
+ process.stdin.on('end',()=>{
65
+ try {
66
+ const j=JSON.parse(d);
67
+ const path='$FIELD'.replace(/^\./,'').split('.');
68
+ let v=j;
69
+ for(const p of path) v=v?.[p];
70
+ process.stdout.write(String(v||''));
71
+ } catch(e) { process.stdout.write(''); }
72
+ });
73
+ " 2>/dev/null
74
+ fi
75
+ }
76
+
77
+ # Read a numeric field from per-session file safely
78
+ read_session_count() {
79
+ local JQ_FILTER="$1"
80
+ local NODE_SCRIPT="$2"
81
+ if command -v jq &>/dev/null; then
82
+ jq "$JQ_FILTER" "$SESSION_FILE" 2>/dev/null || echo "0"
83
+ else
84
+ node -e "$NODE_SCRIPT" 2>/dev/null || echo "0"
85
+ fi
86
+ }
87
+
88
+ TOOL_NAME=$(parse_json "$HOOK_INPUT" ".tool_name")
89
+
90
+ # ============================================================================
91
+ # Filter layer: Is this a consequential action?
92
+ # ============================================================================
93
+
94
+ IS_CONSEQUENTIAL=false
95
+
96
+ case "$TOOL_NAME" in
97
+ Bash)
98
+ # Extract the command being run
99
+ COMMAND=$(parse_json "$HOOK_INPUT" ".tool_input.command")
100
+ # Check for consequential bash commands
101
+ if echo "$COMMAND" | grep -qE '(git\s+push|git\s+tag|npm\s+publish|npx\s+supabase\s+db\s+push|deploy|supabase\s+functions\s+deploy)'; then
102
+ IS_CONSEQUENTIAL=true
103
+ fi
104
+ ;;
105
+ Write|Edit)
106
+ # Check for sensitive file types
107
+ FILE_PATH=$(parse_json "$HOOK_INPUT" ".tool_input.file_path")
108
+ if echo "$FILE_PATH" | grep -qE '\.(sql|env)$'; then
109
+ IS_CONSEQUENTIAL=true
110
+ fi
111
+ ;;
112
+ esac
113
+
114
+ # Not consequential → pass through silently
115
+ if [ "$IS_CONSEQUENTIAL" != "true" ]; then
116
+ exit 0
117
+ fi
118
+
119
+ # ============================================================================
120
+ # CONFIRMATION GATE (runs first — hard block takes priority over soft nag)
121
+ # ============================================================================
122
+ # Block if recall() surfaced scars but confirm_scars() hasn't been called.
123
+ # Only blocks on recall-source scars; session_start scars don't require confirmation.
124
+
125
+ RECALL_SCAR_COUNT=$(read_session_count \
126
+ '[.surfaced_scars // [] | .[] | select(.source == "recall")] | length' \
127
+ "const fs=require('fs');try{const s=JSON.parse(fs.readFileSync('$SESSION_FILE','utf8'));const c=(s.surfaced_scars||[]).filter(x=>x.source==='recall');process.stdout.write(String(c.length))}catch(e){process.stdout.write('0')}")
128
+
129
+ CONFIRMATION_COUNT=$(read_session_count \
130
+ '[.confirmations // [] | .[]] | length' \
131
+ "const fs=require('fs');try{const s=JSON.parse(fs.readFileSync('$SESSION_FILE','utf8'));process.stdout.write(String((s.confirmations||[]).length))}catch(e){process.stdout.write('0')}")
132
+
133
+ if [ "$RECALL_SCAR_COUNT" -gt 0 ] 2>/dev/null && [ "$CONFIRMATION_COUNT" -eq 0 ] 2>/dev/null; then
134
+ # Get scar titles for the error message
135
+ if command -v jq &>/dev/null; then
136
+ SCAR_TITLES=$(jq -r '[.surfaced_scars // [] | .[] | select(.source == "recall") | .scar_title] | join(", ")' "$SESSION_FILE" 2>/dev/null || echo "(unknown)")
137
+ else
138
+ SCAR_TITLES=$(node -e "
139
+ const fs=require('fs');
140
+ try{const s=JSON.parse(fs.readFileSync('$SESSION_FILE','utf8'));
141
+ const t=(s.surfaced_scars||[]).filter(x=>x.source==='recall').map(x=>x.scar_title);
142
+ process.stdout.write(t.join(', '))}catch(e){process.stdout.write('(unknown)')}" 2>/dev/null || echo "(unknown)")
143
+ fi
144
+
145
+ cat <<HOOKJSON
146
+ {
147
+ "decision": "block",
148
+ "reason": "SCAR CONFIRMATION REQUIRED: recall() surfaced ${RECALL_SCAR_COUNT} scar(s) that have not been confirmed. Call confirm_scars() (or gm-confirm) with APPLYING/N_A/REFUTED for each scar before proceeding.\n\nUnconfirmed scars: ${SCAR_TITLES}\n\nEach scar must be addressed with:\n- APPLYING: past-tense evidence of compliance\n- N_A: explain why the scar doesn't apply\n- REFUTED: acknowledge risk of overriding"
149
+ }
150
+ HOOKJSON
151
+ exit 0
152
+ fi
153
+
154
+ # ============================================================================
155
+ # Session state tracking (for nag logic)
156
+ # ============================================================================
157
+
158
+ SESSION_ID="${CLAUDE_SESSION_ID:-$$}"
159
+ STATE_DIR="/tmp/gitmem-hooks-${SESSION_ID}"
160
+ mkdir -p "$STATE_DIR"
161
+
162
+ # Increment tool call count
163
+ TOOL_COUNT=0
164
+ if [ -f "$STATE_DIR/tool_call_count" ]; then
165
+ TOOL_COUNT=$(cat "$STATE_DIR/tool_call_count")
166
+ fi
167
+ TOOL_COUNT=$((TOOL_COUNT + 1))
168
+ echo "$TOOL_COUNT" > "$STATE_DIR/tool_call_count"
169
+
170
+ # ============================================================================
171
+ # Cooldown check: don't nag more than once per 60 seconds
172
+ # ============================================================================
173
+
174
+ NOW=$(date +%s)
175
+ LAST_NAG=0
176
+ if [ -f "$STATE_DIR/last_nag_time" ]; then
177
+ LAST_NAG=$(cat "$STATE_DIR/last_nag_time")
178
+ fi
179
+
180
+ ELAPSED_SINCE_NAG=$((NOW - LAST_NAG))
181
+ if [ "$ELAPSED_SINCE_NAG" -lt 60 ]; then
182
+ exit 0
183
+ fi
184
+
185
+ # ============================================================================
186
+ # RECALL NAG: Nudge if recall hasn't been called
187
+ # ============================================================================
188
+ # Check if any recall-source scars exist. If RECALL_SCAR_COUNT is 0 and
189
+ # we've had >3 tool calls, the agent hasn't called recall at all → nag.
190
+
191
+ SHOULD_NAG=false
192
+
193
+ if [ "$RECALL_SCAR_COUNT" -eq 0 ] 2>/dev/null; then
194
+ # No recall scars found — recall probably wasn't called
195
+ if [ "$TOOL_COUNT" -gt 3 ]; then
196
+ SHOULD_NAG=true
197
+ fi
198
+ fi
199
+
200
+ # ============================================================================
201
+ # Output nag or pass through
202
+ # ============================================================================
203
+
204
+ if [ "$SHOULD_NAG" = "true" ]; then
205
+ echo "$NOW" > "$STATE_DIR/last_nag_time"
206
+ cat <<'HOOKJSON'
207
+ {
208
+ "additionalContext": "GITMEM RECALL REMINDER: You're about to take a consequential action but haven't checked institutional memory recently. Consider calling `recall` (or `gitmem-r`) with your plan before proceeding. This surfaces relevant scars that may prevent repeating past mistakes."
209
+ }
210
+ HOOKJSON
211
+ fi
212
+
213
+ exit 0
@@ -0,0 +1,116 @@
1
+ #!/bin/bash
2
+ # GitMem Hooks Plugin — Stop Hook (Session Close Check)
3
+ # Ensures session_close is called before the session ends.
4
+ #
5
+ # Logic:
6
+ # 1. Check if session is meaningful (registry has entries, or >5 tool calls, or >5 min)
7
+ # 2. If session was closed properly (registry empty) → allow stop
8
+ # 3. If meaningful but not closed → block with reminder
9
+ # 4. Infinite loop guard: if stop_hook_active flag set → always allow
10
+ # 5. Trivial sessions skip enforcement
11
+ #
12
+ # Input: JSON via stdin (hook event data)
13
+ # Output: JSON with decision: "block" and message if enforcement triggered
14
+
15
+ set -e
16
+
17
+ # Read hook input from stdin
18
+ HOOK_INPUT=$(cat -)
19
+
20
+ # ============================================================================
21
+ # Infinite loop guard
22
+ # ============================================================================
23
+
24
+ SESSION_ID="${CLAUDE_SESSION_ID:-$$}"
25
+ STATE_DIR="/tmp/gitmem-hooks-${SESSION_ID}"
26
+
27
+ if [ -f "$STATE_DIR/stop_hook_active" ]; then
28
+ # Already fired once and blocked — don't block again
29
+ exit 0
30
+ fi
31
+
32
+ # ============================================================================
33
+ # Check if session is meaningful
34
+ # ============================================================================
35
+
36
+ IS_MEANINGFUL=false
37
+
38
+ ACTIVE_SESSIONS=".gitmem/active-sessions.json"
39
+ SESSION_STARTED=false
40
+ if [ -f "$ACTIVE_SESSIONS" ]; then
41
+ # Check if registry has active session entries (Phase 1 multi-session, GIT-19)
42
+ if command -v jq &>/dev/null; then
43
+ [ "$(jq '.sessions | length' "$ACTIVE_SESSIONS" 2>/dev/null || echo 0)" -gt 0 ] 2>/dev/null && SESSION_STARTED=true
44
+ elif command -v node &>/dev/null; then
45
+ [ "$(node -e "const fs=require('fs');try{const r=JSON.parse(fs.readFileSync('$ACTIVE_SESSIONS','utf8'));process.stdout.write(String((r.sessions||[]).length))}catch(e){process.stdout.write('0')}" 2>/dev/null)" -gt 0 ] 2>/dev/null && SESSION_STARTED=true
46
+ fi
47
+ fi
48
+
49
+ # Without state dir, we have no tracking data — can't determine meaningfulness.
50
+ # This happens when the SessionStart hook didn't fire (plugin hooks issue).
51
+ # Don't block with bogus defaults — skip enforcement gracefully.
52
+ if [ ! -d "$STATE_DIR" ]; then
53
+ if [ "$SESSION_STARTED" = "true" ]; then
54
+ # Session exists but no tracking — create state dir NOW so future
55
+ # Stop attempts can track from this point forward.
56
+ mkdir -p "$STATE_DIR"
57
+ date +%s > "$STATE_DIR/start_time"
58
+ echo "0" > "$STATE_DIR/tool_call_count"
59
+ fi
60
+ # First time seeing this — not meaningful yet
61
+ exit 0
62
+ fi
63
+
64
+ # Criterion 1: >5 tool calls
65
+ TOOL_COUNT=0
66
+ if [ -f "$STATE_DIR/tool_call_count" ]; then
67
+ TOOL_COUNT=$(cat "$STATE_DIR/tool_call_count" 2>/dev/null || echo "0")
68
+ fi
69
+ if [ "$TOOL_COUNT" -gt 5 ]; then
70
+ IS_MEANINGFUL=true
71
+ fi
72
+
73
+ # Criterion 2: >5 minutes duration
74
+ NOW=$(date +%s)
75
+ if [ -f "$STATE_DIR/start_time" ]; then
76
+ START_TIME=$(cat "$STATE_DIR/start_time" 2>/dev/null || echo "$NOW")
77
+ DURATION=$((NOW - START_TIME))
78
+ if [ "$DURATION" -gt 300 ]; then
79
+ IS_MEANINGFUL=true
80
+ fi
81
+ fi
82
+
83
+ # ============================================================================
84
+ # Trivial session → skip enforcement
85
+ # ============================================================================
86
+
87
+ if [ "$IS_MEANINGFUL" != "true" ]; then
88
+ exit 0
89
+ fi
90
+
91
+ # ============================================================================
92
+ # Check if session was closed properly
93
+ # ============================================================================
94
+
95
+ if [ "$SESSION_STARTED" = "true" ]; then
96
+ # Registry still has entries → session not closed
97
+ # Set guard flag to prevent infinite blocking
98
+ mkdir -p "$STATE_DIR"
99
+ touch "$STATE_DIR/stop_hook_active"
100
+
101
+ cat <<'HOOKJSON'
102
+ {
103
+ "decision": "block",
104
+ "reason": "GITMEM SESSION STILL OPEN — Run the standard closing ceremony:\n\n1. YOU (the agent) ANSWER these 7 reflection questions based on the session. Display your answers to the human:\n - what_broke: What broke that you didn't expect?\n - what_took_longer: What took longer than it should have?\n - do_differently: What would you do differently next time?\n - what_worked: What pattern or approach worked well?\n - wrong_assumption: What assumption was wrong?\n - scars_applied: Which scars or institutional knowledge did you apply?\n - institutional_memory: What from this session should be captured?\n\n2. ASK the human: 'Any corrections or additions to my answers?' WAIT for their response.\n\n3. WRITE structured payload to .gitmem/closing-payload.json with closing_reflection (7 fields above, incorporating human corrections), task_completion (timestamps), and human_corrections.\n\n4. CALL session_close with session_id and close_type: 'standard'.\n\nFor trivial sessions (< 30min, exploratory only), use close_type: 'quick' instead — no questions needed."
105
+ }
106
+ HOOKJSON
107
+ exit 0
108
+ fi
109
+
110
+ # Session was closed properly (registry empty) or never started
111
+ # Clean up state dir
112
+ if [ -d "$STATE_DIR" ]; then
113
+ rm -rf "$STATE_DIR"
114
+ fi
115
+
116
+ exit 0