gitmem-mcp 1.2.1 → 1.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.
package/bin/uninstall.js CHANGED
@@ -28,6 +28,35 @@ const deleteAll = args.includes("--all");
28
28
  const clientIdx = args.indexOf("--client");
29
29
  const clientFlag = clientIdx !== -1 ? args[clientIdx + 1]?.toLowerCase() : null;
30
30
 
31
+ // ── Colors (brand-matched to init wizard) ──
32
+
33
+ const _color =
34
+ !process.env.NO_COLOR &&
35
+ !process.env.GITMEM_NO_COLOR &&
36
+ process.stdout.isTTY;
37
+
38
+ const C = {
39
+ reset: _color ? "\x1b[0m" : "",
40
+ bold: _color ? "\x1b[1m" : "",
41
+ dim: _color ? "\x1b[2m" : "",
42
+ red: _color ? "\x1b[31m" : "",
43
+ green: _color ? "\x1b[32m" : "",
44
+ yellow: _color ? "\x1b[33m" : "",
45
+ };
46
+
47
+ const RIPPLE = `${C.dim}(${C.reset}${C.red}(${C.reset}${C.bold}\u25cf${C.reset}${C.red})${C.reset}${C.dim})${C.reset}`;
48
+ const PRODUCT = `${RIPPLE} ${C.red}gitmem${C.reset}`;
49
+ const CHECK = `${C.bold}\u2714${C.reset}`;
50
+ const SKIP = `${C.dim}\u00b7${C.reset}`;
51
+
52
+ let actionsTaken = 0;
53
+
54
+ function log(icon, msg, extra) {
55
+ if (icon === CHECK) actionsTaken++;
56
+ const suffix = extra ? ` ${C.dim}${extra}${C.reset}` : "";
57
+ console.log(`${icon} ${msg}${suffix}`);
58
+ }
59
+
31
60
  // ── Client Configuration ──
32
61
 
33
62
  const CLIENT_CONFIGS = {
@@ -125,13 +154,11 @@ function writeJson(path, data) {
125
154
  }
126
155
 
127
156
  function isGitmemHook(entry) {
128
- // Claude Code format: entry.hooks is an array of {command: "..."}
129
157
  if (entry.hooks && Array.isArray(entry.hooks)) {
130
158
  return entry.hooks.some(
131
159
  (h) => typeof h.command === "string" && h.command.includes("gitmem")
132
160
  );
133
161
  }
134
- // Cursor format: entry itself has {command: "..."}
135
162
  if (typeof entry.command === "string") {
136
163
  return entry.command.includes("gitmem");
137
164
  }
@@ -142,49 +169,47 @@ function isGitmemHook(entry) {
142
169
 
143
170
  function stepInstructions() {
144
171
  if (!existsSync(cc.instructionsFile)) {
145
- console.log(` No ${cc.instructionsName} found. Skipping.`);
172
+ log(SKIP, `No ${cc.instructionsName} found`);
146
173
  return;
147
174
  }
148
175
 
149
176
  let content = readFileSync(cc.instructionsFile, "utf-8");
150
177
 
151
178
  if (!content.includes(cc.startMarker)) {
152
- console.log(` No gitmem section in ${cc.instructionsName}. Skipping.`);
179
+ log(SKIP, `No gitmem section in ${cc.instructionsName}`);
153
180
  return;
154
181
  }
155
182
 
156
183
  const startIdx = content.indexOf(cc.startMarker);
157
184
  const endIdx = content.indexOf(cc.endMarker);
158
185
  if (startIdx === -1 || endIdx === -1) {
159
- console.log(` Malformed gitmem markers in ${cc.instructionsName}. Skipping.`);
186
+ log(SKIP, `Malformed gitmem markers in ${cc.instructionsName}`);
160
187
  return;
161
188
  }
162
189
 
163
- // Remove the block including markers and surrounding whitespace
164
190
  const before = content.slice(0, startIdx).trimEnd();
165
191
  const after = content.slice(endIdx + cc.endMarker.length).trimStart();
166
-
167
192
  const result = before + (before && after ? "\n\n" : "") + after;
168
193
 
169
194
  if (result.trim() === "") {
170
195
  rmSync(cc.instructionsFile);
171
- console.log(` Removed ${cc.instructionsName} (was gitmem-only)`);
196
+ log(CHECK, `Removed ${cc.instructionsName}`, "(was gitmem-only)");
172
197
  } else {
173
198
  writeFileSync(cc.instructionsFile, result.trimEnd() + "\n");
174
- console.log(` Stripped gitmem section from ${cc.instructionsName}`);
199
+ log(CHECK, `Stripped gitmem section from ${cc.instructionsName}`, "(your content preserved)");
175
200
  }
176
201
  }
177
202
 
178
203
  function stepMcpJson() {
179
204
  const config = readJson(cc.mcpConfigPath);
180
205
  if (!config?.mcpServers) {
181
- console.log(` No ${cc.mcpConfigName} found. Skipping.`);
206
+ log(SKIP, `No ${cc.mcpConfigName} found`);
182
207
  return;
183
208
  }
184
209
 
185
210
  const had = !!config.mcpServers.gitmem || !!config.mcpServers["gitmem-mcp"];
186
211
  if (!had) {
187
- console.log(` No gitmem in ${cc.mcpConfigName}. Skipping.`);
212
+ log(SKIP, `No gitmem in ${cc.mcpConfigName}`);
188
213
  return;
189
214
  }
190
215
 
@@ -193,9 +218,11 @@ function stepMcpJson() {
193
218
 
194
219
  const remaining = Object.keys(config.mcpServers).length;
195
220
  writeJson(cc.mcpConfigPath, config);
196
- console.log(
197
- ` Removed gitmem server (${remaining} other server${remaining !== 1 ? "s" : ""} preserved)`
198
- );
221
+ if (remaining > 0) {
222
+ log(CHECK, `Removed gitmem server`, `(${remaining} other server${remaining !== 1 ? "s" : ""} preserved)`);
223
+ } else {
224
+ log(CHECK, `Removed gitmem server from ${cc.mcpConfigName}`);
225
+ }
199
226
  }
200
227
 
201
228
  function stepHooks() {
@@ -208,7 +235,7 @@ function stepHooks() {
208
235
  function stepHooksClaude() {
209
236
  const settings = readJson(cc.settingsFile);
210
237
  if (!settings?.hooks) {
211
- console.log(" No hooks in .claude/settings.json. Skipping.");
238
+ log(SKIP, "No hooks in .claude/settings.json");
212
239
  return;
213
240
  }
214
241
 
@@ -232,7 +259,7 @@ function stepHooksClaude() {
232
259
  }
233
260
 
234
261
  if (removed === 0) {
235
- console.log(" No gitmem hooks found. Skipping.");
262
+ log(SKIP, "No gitmem hooks found");
236
263
  return;
237
264
  }
238
265
 
@@ -243,18 +270,17 @@ function stepHooksClaude() {
243
270
  }
244
271
 
245
272
  writeJson(cc.settingsFile, settings);
246
- console.log(
247
- ` Removed gitmem hooks` +
248
- (preserved > 0
249
- ? ` (${preserved} other hook${preserved !== 1 ? "s" : ""} preserved)`
250
- : "")
251
- );
273
+ if (preserved > 0) {
274
+ log(CHECK, "Removed automatic memory hooks", `(${preserved} other hook${preserved !== 1 ? "s" : ""} preserved)`);
275
+ } else {
276
+ log(CHECK, "Removed automatic memory hooks");
277
+ }
252
278
  }
253
279
 
254
280
  function stepHooksCursor() {
255
281
  const config = readJson(cc.hooksFile);
256
282
  if (!config?.hooks) {
257
- console.log(` No hooks in ${cc.hooksFileName}. Skipping.`);
283
+ log(SKIP, `No hooks in ${cc.hooksFileName}`);
258
284
  return;
259
285
  }
260
286
 
@@ -278,7 +304,7 @@ function stepHooksCursor() {
278
304
  }
279
305
 
280
306
  if (removed === 0) {
281
- console.log(" No gitmem hooks found. Skipping.");
307
+ log(SKIP, "No gitmem hooks found");
282
308
  return;
283
309
  }
284
310
 
@@ -289,38 +315,36 @@ function stepHooksCursor() {
289
315
  }
290
316
 
291
317
  writeJson(cc.hooksFile, config);
292
- console.log(
293
- ` Removed gitmem hooks` +
294
- (preserved > 0
295
- ? ` (${preserved} other hook${preserved !== 1 ? "s" : ""} preserved)`
296
- : "")
297
- );
318
+ if (preserved > 0) {
319
+ log(CHECK, "Removed automatic memory hooks", `(${preserved} other hook${preserved !== 1 ? "s" : ""} preserved)`);
320
+ } else {
321
+ log(CHECK, "Removed automatic memory hooks");
322
+ }
298
323
  }
299
324
 
300
325
  function stepPermissions() {
301
326
  if (!cc.hasPermissions) {
302
- console.log(` Not needed for ${cc.name}. Skipping.`);
327
+ log(SKIP, `Not needed for ${cc.name}`);
303
328
  return;
304
329
  }
305
330
 
306
331
  const settings = readJson(cc.settingsFile);
307
332
  const allow = settings?.permissions?.allow;
308
333
  if (!Array.isArray(allow)) {
309
- console.log(" No permissions in .claude/settings.json. Skipping.");
334
+ log(SKIP, "No permissions in .claude/settings.json");
310
335
  return;
311
336
  }
312
337
 
313
338
  const pattern = "mcp__gitmem__*";
314
339
  const idx = allow.indexOf(pattern);
315
340
  if (idx === -1) {
316
- console.log(" No gitmem permissions found. Skipping.");
341
+ log(SKIP, "No gitmem permissions found");
317
342
  return;
318
343
  }
319
344
 
320
345
  allow.splice(idx, 1);
321
346
  settings.permissions.allow = allow;
322
347
 
323
- // Clean up empty permissions
324
348
  if (allow.length === 0) {
325
349
  delete settings.permissions.allow;
326
350
  }
@@ -332,31 +356,26 @@ function stepPermissions() {
332
356
  }
333
357
 
334
358
  writeJson(cc.settingsFile, settings);
335
- console.log(" Removed mcp__gitmem__* from permissions.allow");
359
+ log(CHECK, "Removed tool permissions");
336
360
  }
337
361
 
338
362
  async function stepGitmemDir() {
339
363
  if (!existsSync(gitmemDir)) {
340
- console.log(" No .gitmem/ directory. Skipping.");
364
+ log(SKIP, "No .gitmem/ directory");
341
365
  return;
342
366
  }
343
367
 
344
368
  if (deleteAll) {
345
369
  rmSync(gitmemDir, { recursive: true, force: true });
346
- console.log(" Deleted .gitmem/ directory");
370
+ log(CHECK, "Deleted .gitmem/ directory");
347
371
  return;
348
372
  }
349
373
 
350
- console.log(
351
- " This contains your memory data (scars, sessions, decisions)."
352
- );
353
-
354
- // Default to No for data deletion
355
- if (await confirm("Delete .gitmem/?", false)) {
356
- rmSync(gitmemDir, { recursive: true, force: true });
357
- console.log(" Deleted .gitmem/ directory");
374
+ if (await confirm("Keep .gitmem/ memory data for future use?", true)) {
375
+ log(CHECK, ".gitmem/ preserved your memories will be here if you reinstall");
358
376
  } else {
359
- console.log(" Skipped .gitmem/ preserved.");
377
+ rmSync(gitmemDir, { recursive: true, force: true });
378
+ log(CHECK, "Deleted .gitmem/ directory");
360
379
  }
361
380
  }
362
381
 
@@ -366,58 +385,38 @@ function stepGitignore() {
366
385
  let content = readFileSync(gitignorePath, "utf-8");
367
386
  if (!content.includes(".gitmem/")) return;
368
387
 
369
- // Remove the .gitmem/ line
370
388
  const lines = content.split("\n");
371
389
  const filtered = lines.filter((line) => line.trim() !== ".gitmem/");
372
390
  writeFileSync(gitignorePath, filtered.join("\n"));
391
+ log(CHECK, "Cleaned .gitignore");
373
392
  }
374
393
 
375
394
  // ── Main ──
376
395
 
377
396
  async function main() {
397
+ const pkg = readJson(join(__dirname, "..", "package.json"));
398
+ const version = pkg?.version || "1.0.0";
399
+
378
400
  console.log("");
379
- console.log(` gitmem Uninstall (${cc.name})`);
380
- if (clientFlag) {
381
- console.log(` (client: ${client} — via --client flag)`);
382
- } else {
383
- console.log(` (client: ${client} — auto-detected)`);
384
- }
401
+ console.log(`${PRODUCT} \u2500\u2500 uninstall v${version}`);
402
+ console.log(`${C.dim}Removing gitmem from ${cc.name}${clientFlag ? "" : " (auto-detected)"}${C.reset}`);
385
403
  console.log("");
386
404
 
387
- const stepCount = cc.hasPermissions ? 5 : 4;
388
- let step = 1;
389
-
390
- console.log(` Step ${step}/${stepCount} — Remove gitmem section from ${cc.instructionsName}`);
391
405
  stepInstructions();
392
- console.log("");
393
- step++;
394
-
395
- console.log(` Step ${step}/${stepCount} — Remove gitmem from ${cc.mcpConfigName}`);
396
406
  stepMcpJson();
397
- console.log("");
398
- step++;
399
-
400
- const hooksTarget = cc.hooksInSettings ? ".claude/settings.json" : cc.hooksFileName;
401
- console.log(` Step ${step}/${stepCount} — Remove gitmem hooks from ${hooksTarget}`);
402
407
  stepHooks();
403
- console.log("");
404
- step++;
405
-
406
- if (cc.hasPermissions) {
407
- console.log(` Step ${step}/${stepCount} — Remove gitmem permissions from .claude/settings.json`);
408
- stepPermissions();
409
- console.log("");
410
- step++;
411
- }
412
-
413
- console.log(` Step ${step}/${stepCount} — Delete .gitmem/ directory?`);
408
+ stepPermissions();
414
409
  await stepGitmemDir();
415
-
416
- // Also clean .gitignore entry
417
410
  stepGitignore();
418
411
 
419
412
  console.log("");
420
- console.log(" Uninstall complete.");
413
+ if (actionsTaken > 0) {
414
+ console.log(`${C.dim}gitmem-mcp has been removed.${C.reset}`);
415
+ console.log(`${C.dim}Reinstall anytime: ${C.reset}${C.red}npx gitmem-mcp init${C.reset}`);
416
+ } else {
417
+ console.log(`${C.dim}gitmem-mcp is not installed in this project.${C.reset}`);
418
+ console.log(`${C.dim}Install: ${C.reset}${C.red}npx gitmem-mcp init${C.reset}`);
419
+ }
421
420
  console.log("");
422
421
 
423
422
  if (rl) rl.close();
@@ -18,12 +18,12 @@ export declare const AnalyzeParamsSchema: z.ZodObject<{
18
18
  }, "strip", z.ZodTypeAny, {
19
19
  project?: string | undefined;
20
20
  agent?: string | undefined;
21
- lens?: "summary" | "reflections" | "blindspots" | undefined;
21
+ lens?: "reflections" | "summary" | "blindspots" | undefined;
22
22
  days?: number | undefined;
23
23
  }, {
24
24
  project?: string | undefined;
25
25
  agent?: string | undefined;
26
- lens?: "summary" | "reflections" | "blindspots" | undefined;
26
+ lens?: "reflections" | "summary" | "blindspots" | undefined;
27
27
  days?: number | undefined;
28
28
  }>;
29
29
  export type AnalyzeParams = z.infer<typeof AnalyzeParamsSchema>;
package/dist/server.js CHANGED
@@ -18,6 +18,7 @@ import { recordScarUsage } from "./tools/record-scar-usage.js";
18
18
  import { recordScarUsageBatch } from "./tools/record-scar-usage-batch.js";
19
19
  import { recall } from "./tools/recall.js";
20
20
  import { confirmScars } from "./tools/confirm-scars.js";
21
+ import { reflectScars } from "./tools/reflect-scars.js";
21
22
  import { saveTranscript } from "./tools/save-transcript.js";
22
23
  import { getTranscript } from "./tools/get-transcript.js";
23
24
  import { searchTranscripts } from "./tools/search-transcripts.js";
@@ -103,6 +104,11 @@ export function createServer() {
103
104
  case "gm-confirm":
104
105
  result = await confirmScars(toolArgs);
105
106
  break;
107
+ case "reflect_scars":
108
+ case "gitmem-rf":
109
+ case "gm-reflect":
110
+ result = await reflectScars(toolArgs);
111
+ break;
106
112
  case "session_start":
107
113
  case "gitmem-ss":
108
114
  case "gm-open":
@@ -213,6 +219,7 @@ export function createServer() {
213
219
  const commands = [
214
220
  { alias: "gitmem-r", full: "recall", description: "Check scars before taking action" },
215
221
  { alias: "gitmem-cs", full: "confirm_scars", description: "Confirm recalled scars (APPLYING/N_A/REFUTED)" },
222
+ { alias: "gitmem-rf", full: "reflect_scars", description: "End-of-session scar reflection (OBEYED/REFUTED)" },
216
223
  { alias: "gitmem-ss", full: "session_start", description: "Initialize session with context" },
217
224
  { alias: "gitmem-sr", full: "session_refresh", description: "Refresh context for active session" },
218
225
  { alias: "gitmem-sc", full: "session_close", description: "Close session with compliance validation" },
@@ -10,7 +10,7 @@
10
10
  * - Universal: works in ALL MCP clients, no IDE hooks needed
11
11
  * - Lightweight: pure in-memory checks, no I/O
12
12
  */
13
- import { getCurrentSession, hasUnconfirmedScars, getSurfacedScars } from "./session-state.js";
13
+ import { getCurrentSession, hasUnconfirmedScars, getSurfacedScars, isRecallCalled } from "./session-state.js";
14
14
  /**
15
15
  * Tools that require an active session to function properly.
16
16
  * Read-only/administrative tools are excluded.
@@ -107,19 +107,17 @@ export function checkEnforcement(toolName) {
107
107
  };
108
108
  }
109
109
  // Check 3: No recall before consequential action
110
- if (CONSEQUENTIAL_TOOLS.has(toolName)) {
111
- const recallScars = getSurfacedScars().filter(s => s.source === "recall");
112
- if (recallScars.length === 0) {
113
- return {
114
- warning: [
115
- "--- gitmem enforcement ---",
116
- "No recall() was run this session before this action.",
117
- "Consider calling recall() first to check for relevant institutional memory.",
118
- "Past mistakes and patterns may prevent repeating known issues.",
119
- "---",
120
- ].join("\n"),
121
- };
122
- }
110
+ // Uses recallCalled boolean to avoid false positives when recall returns 0 scars
111
+ if (CONSEQUENTIAL_TOOLS.has(toolName) && !isRecallCalled()) {
112
+ return {
113
+ warning: [
114
+ "--- gitmem enforcement ---",
115
+ "No recall() was run this session before this action.",
116
+ "Consider calling recall() first to check for relevant institutional memory.",
117
+ "Past mistakes and patterns may prevent repeating known issues.",
118
+ "---",
119
+ ].join("\n"),
120
+ };
123
121
  }
124
122
  return { warning: null };
125
123
  }
@@ -7,7 +7,7 @@
7
7
  /**
8
8
  * Tool names that can be tracked
9
9
  */
10
- export type ToolName = "recall" | "search" | "log" | "session_start" | "session_refresh" | "session_close" | "create_learning" | "create_decision" | "record_scar_usage" | "record_scar_usage_batch" | "save_transcript" | "get_transcript" | "search_transcripts" | "analyze" | "graph_traverse" | "prepare_context" | "absorb_observations" | "list_threads" | "resolve_thread" | "create_thread" | "confirm_scars" | "cleanup_threads" | "health";
10
+ export type ToolName = "recall" | "search" | "log" | "session_start" | "session_refresh" | "session_close" | "create_learning" | "create_decision" | "record_scar_usage" | "record_scar_usage_batch" | "save_transcript" | "get_transcript" | "search_transcripts" | "analyze" | "graph_traverse" | "prepare_context" | "absorb_observations" | "list_threads" | "resolve_thread" | "create_thread" | "confirm_scars" | "reflect_scars" | "cleanup_threads" | "health";
11
11
  /**
12
12
  * Phase tags for context
13
13
  */
@@ -33,6 +33,7 @@ export const PERFORMANCE_TARGETS = {
33
33
  resolve_thread: 100, // In-memory mutation + file write
34
34
  create_thread: 100, // In-memory mutation + file write
35
35
  confirm_scars: 500, // In-memory validation + file write
36
+ reflect_scars: 500, // In-memory validation + file write
36
37
  cleanup_threads: 2000, // Fetch all threads + lifecycle computation
37
38
  health: 100, // In-memory read from EffectTracker
38
39
  };
@@ -11,15 +11,17 @@
11
11
  *
12
12
  * This allows recall() to always assign variants even without explicit parameters.
13
13
  */
14
- import type { SurfacedScar, ScarConfirmation, Observation, SessionChild, ThreadObject } from "../types/index.js";
14
+ import type { SurfacedScar, ScarConfirmation, ScarReflection, Observation, SessionChild, ThreadObject } from "../types/index.js";
15
15
  interface SessionContext {
16
16
  sessionId: string;
17
17
  linearIssue?: string;
18
18
  agent?: string;
19
19
  project?: string;
20
20
  startedAt: Date;
21
+ recallCalled: boolean;
21
22
  surfacedScars: SurfacedScar[];
22
23
  confirmations: ScarConfirmation[];
24
+ reflections: ScarReflection[];
23
25
  observations: Observation[];
24
26
  children: SessionChild[];
25
27
  threads: ThreadObject[];
@@ -28,7 +30,7 @@ interface SessionContext {
28
30
  * Set the current active session
29
31
  * Called by session_start
30
32
  */
31
- export declare function setCurrentSession(context: Omit<SessionContext, 'surfacedScars' | 'confirmations' | 'observations' | 'children' | 'threads'> & {
33
+ export declare function setCurrentSession(context: Omit<SessionContext, 'recallCalled' | 'surfacedScars' | 'confirmations' | 'reflections' | 'observations' | 'children' | 'threads'> & {
32
34
  surfacedScars?: SurfacedScar[];
33
35
  observations?: Observation[];
34
36
  children?: SessionChild[];
@@ -53,6 +55,16 @@ export declare function getProject(): string | null;
53
55
  * Check if currently working on a Linear issue
54
56
  */
55
57
  export declare function hasActiveIssue(): boolean;
58
+ /**
59
+ * Mark that recall() was called this session (independent of whether it returned scars).
60
+ * Called by recall tool before any early return.
61
+ */
62
+ export declare function setRecallCalled(): void;
63
+ /**
64
+ * Check if recall() was called this session.
65
+ * Used by enforcement to avoid false positives when recall returns 0 scars.
66
+ */
67
+ export declare function isRecallCalled(): boolean;
56
68
  /**
57
69
  * Add surfaced scars to tracking (deduplicates by scar_id)
58
70
  * Called by session_start and recall when scars are surfaced.
@@ -71,6 +83,15 @@ export declare function addConfirmations(confirmations: ScarConfirmation[]): voi
71
83
  * Get all scar confirmations for the current session.
72
84
  */
73
85
  export declare function getConfirmations(): ScarConfirmation[];
86
+ /**
87
+ * Add end-of-session scar reflections (OBEYED/REFUTED) to the current session.
88
+ * Called by reflect_scars tool after validation.
89
+ */
90
+ export declare function addReflections(reflections: ScarReflection[]): void;
91
+ /**
92
+ * Get all end-of-session scar reflections for the current session.
93
+ */
94
+ export declare function getReflections(): ScarReflection[];
74
95
  /**
75
96
  * Check if there are recall-surfaced scars that haven't been confirmed.
76
97
  * Only checks scars with source "recall" — session_start scars don't require confirmation.
@@ -20,8 +20,10 @@ let currentSession = null;
20
20
  export function setCurrentSession(context) {
21
21
  currentSession = {
22
22
  ...context,
23
+ recallCalled: false,
23
24
  surfacedScars: context.surfacedScars || [],
24
25
  confirmations: [],
26
+ reflections: [],
25
27
  observations: context.observations || [],
26
28
  children: context.children || [],
27
29
  threads: context.threads || [],
@@ -58,6 +60,23 @@ export function getProject() {
58
60
  export function hasActiveIssue() {
59
61
  return !!(currentSession?.linearIssue);
60
62
  }
63
+ /**
64
+ * Mark that recall() was called this session (independent of whether it returned scars).
65
+ * Called by recall tool before any early return.
66
+ */
67
+ export function setRecallCalled() {
68
+ if (currentSession) {
69
+ currentSession.recallCalled = true;
70
+ console.error("[session-state] recall() marked as called");
71
+ }
72
+ }
73
+ /**
74
+ * Check if recall() was called this session.
75
+ * Used by enforcement to avoid false positives when recall returns 0 scars.
76
+ */
77
+ export function isRecallCalled() {
78
+ return currentSession?.recallCalled ?? false;
79
+ }
61
80
  /**
62
81
  * Add surfaced scars to tracking (deduplicates by scar_id)
63
82
  * Called by session_start and recall when scars are surfaced.
@@ -108,6 +127,33 @@ export function addConfirmations(confirmations) {
108
127
  export function getConfirmations() {
109
128
  return currentSession?.confirmations || [];
110
129
  }
130
+ /**
131
+ * Add end-of-session scar reflections (OBEYED/REFUTED) to the current session.
132
+ * Called by reflect_scars tool after validation.
133
+ */
134
+ export function addReflections(reflections) {
135
+ if (!currentSession) {
136
+ console.warn("[session-state] Cannot add reflections: no active session");
137
+ return;
138
+ }
139
+ for (const ref of reflections) {
140
+ // Replace existing reflection for same scar_id (allow re-reflection)
141
+ const idx = currentSession.reflections.findIndex(r => r.scar_id === ref.scar_id);
142
+ if (idx >= 0) {
143
+ currentSession.reflections[idx] = ref;
144
+ }
145
+ else {
146
+ currentSession.reflections.push(ref);
147
+ }
148
+ }
149
+ console.error(`[session-state] Reflections tracked: ${currentSession.reflections.length} total`);
150
+ }
151
+ /**
152
+ * Get all end-of-session scar reflections for the current session.
153
+ */
154
+ export function getReflections() {
155
+ return currentSession?.reflections || [];
156
+ }
111
157
  /**
112
158
  * Check if there are recall-surfaced scars that haven't been confirmed.
113
159
  * Only checks scars with source "recall" — session_start scars don't require confirmation.