gitmem-mcp 1.0.15 → 1.1.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.
@@ -56,8 +56,10 @@ export function normalizeThreads(raw, sourceSession) {
56
56
  id: parsed.id,
57
57
  text: parsed.note,
58
58
  status: parsed.status,
59
+ // Preserve existing created_at — only default to now() for genuinely new threads
59
60
  created_at: parsed.created_at || new Date().toISOString(),
60
- ...(sourceSession && { source_session: sourceSession }),
61
+ ...(sourceSession && !parsed.source_session && { source_session: sourceSession }),
62
+ ...(parsed.source_session && { source_session: parsed.source_session }),
61
63
  ...(parsed.resolved_at && { resolved_at: parsed.resolved_at }),
62
64
  };
63
65
  }
@@ -88,8 +90,9 @@ export function normalizeThreads(raw, sourceSession) {
88
90
  id: inner.id,
89
91
  text: inner.text || inner.note,
90
92
  status: inner.status || item.status,
93
+ // Preserve the earliest available created_at — never overwrite with now()
91
94
  created_at: inner.created_at || item.created_at || new Date().toISOString(),
92
- ...(sourceSession && { source_session: sourceSession }),
95
+ ...(sourceSession && !item.source_session && { source_session: sourceSession }),
93
96
  ...(inner.resolved_at && { resolved_at: inner.resolved_at }),
94
97
  };
95
98
  }
@@ -15,7 +15,7 @@
15
15
  * N_A — Scar doesn't apply, scenario comparison required
16
16
  * REFUTED — Overriding scar, risk acknowledgment required
17
17
  */
18
- import { getCurrentSession, getSurfacedScars, addConfirmations, } from "../services/session-state.js";
18
+ import { getCurrentSession, getSurfacedScars, addConfirmations, getConfirmations, } from "../services/session-state.js";
19
19
  import { Timer, buildPerformanceData } from "../services/metrics.js";
20
20
  import { getSessionPath } from "../services/gitmem-dir.js";
21
21
  import { wrapDisplay } from "../services/display-protocol.js";
@@ -23,7 +23,8 @@ import * as fs from "fs";
23
23
  // Minimum evidence length per decision type
24
24
  const MIN_EVIDENCE_LENGTH = 50;
25
25
  // Future-tense patterns — APPLYING must use past tense
26
- const FUTURE_PATTERNS = /\b(will|going to|plan to|intend to|I'll|we'll|shall|about to|aim to|expect to)\b/i;
26
+ // Only catch first-person forward-looking language, not third-person "will"
27
+ const FUTURE_PATTERNS = /\b(I will|I'll|we will|we'll|I'm going to|we're going to|I plan to|I intend to|I shall|I aim to|I expect to)\b/i;
27
28
  /**
28
29
  * Validate a single confirmation against its surfaced scar.
29
30
  * Returns null if valid, or an error string if invalid.
@@ -192,9 +193,11 @@ export async function confirmScars(params) {
192
193
  }
193
194
  }
194
195
  // Check for missing scars (all recall scars must be addressed)
196
+ // Credit scars already confirmed in a previous call this session
197
+ const previouslyConfirmedIds = new Set(getConfirmations().map(c => c.scar_id));
195
198
  const missingScars = [];
196
199
  for (const scar of recallScars) {
197
- if (!confirmedIds.has(scar.scar_id)) {
200
+ if (!confirmedIds.has(scar.scar_id) && !previouslyConfirmedIds.has(scar.scar_id)) {
198
201
  missingScars.push(scar.scar_title);
199
202
  }
200
203
  }
@@ -172,7 +172,9 @@ export async function recall(params) {
172
172
  const matchCount = params.match_count || 3;
173
173
  const issueId = params.issue_id; // For variant assignment
174
174
  // Similarity threshold — suppress weak matches
175
- const defaultThreshold = hasSupabase() ? 0.35 : 0.4;
175
+ // Pro tier: 0.45 calibrated from UX audit (66% N_A rate at 0.35, APPLYING avg 0.55, N_A avg 0.51)
176
+ // Free tier: 0.4 (BM25 scores are relative — top result always 1.0)
177
+ const defaultThreshold = hasSupabase() ? 0.45 : 0.4;
176
178
  const similarityThreshold = params.similarity_threshold ?? defaultThreshold;
177
179
  // Free tier: use local keyword search
178
180
  if (!hasSupabase()) {
@@ -1,21 +1,23 @@
1
1
  #!/bin/bash
2
2
  # GitMem Hooks Plugin — PreToolUse Hook (Recall Check + Confirmation Gate)
3
3
  #
4
- # Two enforcement mechanisms for consequential actions:
4
+ # Two enforcement mechanisms with different trigger scopes:
5
5
  #
6
- # 1. CONFIRMATION GATE (hard block):
6
+ # 1. CONFIRMATION GATE (hard block, consequential actions only):
7
7
  # If recall() surfaced scars but confirm_scars() hasn't been called → BLOCK.
8
8
  # Uses JSON "decision: block" pattern (same as session-close-check.sh).
9
9
  # Only blocks on recall-source scars; session_start scars don't require confirmation.
10
+ # Consequential = git push, npm publish, deploy, .sql, .env files.
10
11
  #
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
12
+ # 2. RECALL NAG (soft reminder, ALL Bash/Write/Edit actions):
13
+ # If recall hasn't been called AND agent has made 10+ tool calls → nudge.
14
+ # Never blocks just injects additionalContext.
15
+ # Cooldown: no more than once per 90 seconds.
16
+ # NOTE: hooks.json matchers already limit this to Bash/Write/Edit.
15
17
  #
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
18
+ # UX audit finding: 12 sessions >30min with zero recalls because the nag was
19
+ # gated behind the consequential filter agents writing .ts files, running
20
+ # tests, etc. were never nudged. Now the nag fires for all code changes.
19
21
  #
20
22
  # Input: JSON via stdin with tool_name and tool_input
21
23
  # Output: JSON with decision:block OR additionalContext OR empty (exit 0)
@@ -88,7 +90,35 @@ read_session_count() {
88
90
  TOOL_NAME=$(parse_json "$HOOK_INPUT" ".tool_name")
89
91
 
90
92
  # ============================================================================
91
- # Filter layer: Is this a consequential action?
93
+ # Read session state (shared by both gate and nag)
94
+ # ============================================================================
95
+
96
+ RECALL_SCAR_COUNT=$(read_session_count \
97
+ '[.surfaced_scars // [] | .[] | select(.source == "recall")] | length' \
98
+ "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')}")
99
+
100
+ CONFIRMATION_COUNT=$(read_session_count \
101
+ '[.confirmations // [] | .[]] | length' \
102
+ "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')}")
103
+
104
+ # ============================================================================
105
+ # Session state tracking (for nag logic — runs for ALL matched tool calls)
106
+ # ============================================================================
107
+
108
+ SESSION_ID="${CLAUDE_SESSION_ID:-$$}"
109
+ STATE_DIR="/tmp/gitmem-hooks-${SESSION_ID}"
110
+ mkdir -p "$STATE_DIR"
111
+
112
+ # Increment tool call count (counts all Bash/Write/Edit, not just consequential)
113
+ TOOL_COUNT=0
114
+ if [ -f "$STATE_DIR/tool_call_count" ]; then
115
+ TOOL_COUNT=$(cat "$STATE_DIR/tool_call_count")
116
+ fi
117
+ TOOL_COUNT=$((TOOL_COUNT + 1))
118
+ echo "$TOOL_COUNT" > "$STATE_DIR/tool_call_count"
119
+
120
+ # ============================================================================
121
+ # Filter layer: Is this a consequential action? (used for gate only)
92
122
  # ============================================================================
93
123
 
94
124
  IS_CONSEQUENTIAL=false
@@ -111,26 +141,13 @@ case "$TOOL_NAME" in
111
141
  ;;
112
142
  esac
113
143
 
114
- # Not consequential → pass through silently
115
- if [ "$IS_CONSEQUENTIAL" != "true" ]; then
116
- exit 0
117
- fi
118
-
119
144
  # ============================================================================
120
- # CONFIRMATION GATE (runs first — hard block takes priority over soft nag)
145
+ # CONFIRMATION GATE (hard block consequential actions only)
121
146
  # ============================================================================
122
147
  # Block if recall() surfaced scars but confirm_scars() hasn't been called.
123
148
  # Only blocks on recall-source scars; session_start scars don't require confirmation.
124
149
 
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
150
+ if [ "$IS_CONSEQUENTIAL" = "true" ] && [ "$RECALL_SCAR_COUNT" -gt 0 ] 2>/dev/null && [ "$CONFIRMATION_COUNT" -eq 0 ] 2>/dev/null; then
134
151
  # Get scar titles for the error message
135
152
  if command -v jq &>/dev/null; then
136
153
  SCAR_TITLES=$(jq -r '[.surfaced_scars // [] | .[] | select(.source == "recall") | .scar_title] | join(", ")' "$SESSION_FILE" 2>/dev/null || echo "(unknown)")
@@ -152,24 +169,11 @@ HOOKJSON
152
169
  fi
153
170
 
154
171
  # ============================================================================
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
+ # RECALL NAG (soft reminder — ALL Bash/Write/Edit, not just consequential)
172
173
  # ============================================================================
174
+ # UX audit: 12 sessions >30min with zero recalls because nag was gated behind
175
+ # consequential filter. Now fires for any code change after 10+ tool calls.
176
+ # Cooldown: 90s between nags. Never blocks — additionalContext only.
173
177
 
174
178
  NOW=$(date +%s)
175
179
  LAST_NAG=0
@@ -178,21 +182,16 @@ if [ -f "$STATE_DIR/last_nag_time" ]; then
178
182
  fi
179
183
 
180
184
  ELAPSED_SINCE_NAG=$((NOW - LAST_NAG))
181
- if [ "$ELAPSED_SINCE_NAG" -lt 60 ]; then
185
+ if [ "$ELAPSED_SINCE_NAG" -lt 90 ]; then
182
186
  exit 0
183
187
  fi
184
188
 
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
189
  SHOULD_NAG=false
192
190
 
193
191
  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
192
+ # No recall scars found — recall hasn't been called this session
193
+ # Threshold: 10 tool calls to avoid nagging during initial exploration
194
+ if [ "$TOOL_COUNT" -gt 10 ]; then
196
195
  SHOULD_NAG=true
197
196
  fi
198
197
  fi
@@ -205,7 +204,7 @@ if [ "$SHOULD_NAG" = "true" ]; then
205
204
  echo "$NOW" > "$STATE_DIR/last_nag_time"
206
205
  cat <<'HOOKJSON'
207
206
  {
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."
207
+ "additionalContext": "GITMEM RECALL REMINDER: You've been working for a while without checking institutional memory. Consider calling `recall` (or `gitmem-r`) with your current plan it surfaces relevant lessons from past sessions that may save time or prevent mistakes. Example: recall({ plan: \"what I'm about to do\" })"
209
208
  }
210
209
  HOOKJSON
211
210
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitmem-mcp",
3
- "version": "1.0.15",
3
+ "version": "1.1.0",
4
4
  "description": "Institutional memory for AI coding agents. Memory that compounds.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -55,6 +55,7 @@
55
55
  "!hooks/tests",
56
56
  "schema",
57
57
  "CLAUDE.md.template",
58
+ "cursorrules.template",
58
59
  "README.md",
59
60
  "CHANGELOG.md"
60
61
  ],