gitmem-mcp 1.1.2 → 1.1.3

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/CHANGELOG.md CHANGED
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.1.3] - 2026-02-19
11
+
12
+ ### Fixed
13
+ - **Thread display output**: `list_threads` and `cleanup_threads` replaced ASCII box-drawing tables with markdown tables. Thread text truncation increased from 40-48 to 60 characters. Output now renders cleanly in all MCP clients instead of clipping on narrow terminals.
14
+
10
15
  ## [1.1.2] - 2026-02-17
11
16
 
12
17
  ### Changed
package/README.md CHANGED
@@ -21,7 +21,9 @@
21
21
 
22
22
  GitMem is an [MCP server](https://modelcontextprotocol.io/) that gives your AI coding agent **persistent memory across sessions**. It remembers mistakes (scars), successes (wins), and decisions — so your agent learns from experience instead of starting from scratch every time.
23
23
 
24
- Works with **Claude Code**, **Claude Desktop**, **Cursor**, and any MCP-compatible client.
24
+ > **What's MCP?** [Model Context Protocol](https://modelcontextprotocol.io/) is how AI coding tools connect to external capabilities. GitMem is an MCP server — install it once and your agent gains persistent memory.
25
+
26
+ Works with **Claude Code**, **Cursor**, **VS Code (Copilot)**, **Windsurf**, and any MCP-compatible client.
25
27
 
26
28
  ## Quick Start
27
29
 
@@ -29,19 +31,19 @@ Works with **Claude Code**, **Claude Desktop**, **Cursor**, and any MCP-compatib
29
31
  npx gitmem-mcp init
30
32
  ```
31
33
 
32
- One command. The wizard sets up everything:
33
- - `.gitmem/` directory with 3 starter scars
34
- - `.mcp.json` with gitmem server entry
35
- - `CLAUDE.md` with memory protocol instructions
36
- - `.claude/settings.json` with tool permissions
37
- - Lifecycle hooks for automatic session management
34
+ One command. The wizard auto-detects your IDE and sets up everything:
35
+ - `.gitmem/` directory with starter scars
36
+ - MCP server config (`.mcp.json`, `.vscode/mcp.json`, `.cursor/mcp.json`, etc.)
37
+ - Instructions file (`CLAUDE.md`, `.cursorrules`, `.windsurfrules`, `.github/copilot-instructions.md`)
38
+ - Lifecycle hooks (where supported)
38
39
  - `.gitignore` updated
39
40
 
40
41
  Already have existing config? The wizard merges without destroying anything. Re-running is safe.
41
42
 
42
43
  ```bash
43
- npx gitmem-mcp init --yes # Non-interactive
44
- npx gitmem-mcp init --dry-run # Preview changes
44
+ npx gitmem-mcp init --yes # Non-interactive
45
+ npx gitmem-mcp init --dry-run # Preview changes
46
+ npx gitmem-mcp init --client vscode # Force specific client
45
47
  ```
46
48
 
47
49
  ## How It Works
@@ -78,16 +80,22 @@ Every scar includes **counter-arguments** — reasons why someone might reasonab
78
80
 
79
81
  ## Supported Clients
80
82
 
81
- | Client | Setup |
82
- |--------|-------|
83
- | **Claude Code** | `npx gitmem-mcp init` (auto-detected) |
84
- | **Claude Desktop** | `npx gitmem-mcp init` or add to `claude_desktop_config.json` |
85
- | **Cursor** | `npx gitmem-mcp init` or add to `.cursor/mcp.json` |
86
- | **Any MCP client** | Add `npx -y gitmem-mcp` as an MCP server |
83
+ | Client | Setup | Hooks |
84
+ |--------|-------|-------|
85
+ | **Claude Code** | `npx gitmem-mcp init` | Full (session, recall, credential guard) |
86
+ | **Cursor** | `npx gitmem-mcp init --client cursor` | Partial (session, recall) |
87
+ | **VS Code (Copilot)** | `npx gitmem-mcp init --client vscode` | Instructions-based |
88
+ | **Windsurf** | `npx gitmem-mcp init --client windsurf` | Instructions-based |
89
+ | **Claude Desktop** | Add to `claude_desktop_config.json` | Manual |
90
+ | **Any MCP client** | `npx gitmem-mcp init --client generic` | Instructions-based |
91
+
92
+ The wizard auto-detects your IDE. Use `--client` to override.
87
93
 
88
94
  <details>
89
95
  <summary><strong>Manual MCP configuration</strong></summary>
90
96
 
97
+ Add this to your MCP client's config file:
98
+
91
99
  ```json
92
100
  {
93
101
  "mcpServers": {
@@ -99,13 +107,21 @@ Every scar includes **counter-arguments** — reasons why someone might reasonab
99
107
  }
100
108
  ```
101
109
 
110
+ | Client | Config file |
111
+ |--------|-------------|
112
+ | Claude Code | `.mcp.json` |
113
+ | Cursor | `.cursor/mcp.json` |
114
+ | VS Code | `.vscode/mcp.json` |
115
+ | Windsurf | `~/.codeium/windsurf/mcp_config.json` |
116
+
102
117
  </details>
103
118
 
104
119
  ## CLI Commands
105
120
 
106
121
  | Command | Description |
107
122
  |---------|-------------|
108
- | `npx gitmem-mcp init` | Interactive setup wizard |
123
+ | `npx gitmem-mcp init` | Interactive setup wizard (auto-detects IDE) |
124
+ | `npx gitmem-mcp init --client <name>` | Setup for specific client (`claude`, `cursor`, `vscode`, `windsurf`, `generic`) |
109
125
  | `npx gitmem-mcp init --yes` | Non-interactive setup |
110
126
  | `npx gitmem-mcp init --dry-run` | Preview changes |
111
127
  | `npx gitmem-mcp uninstall` | Clean removal (preserves `.gitmem/` data) |
@@ -6,7 +6,7 @@
6
6
  * Interactive setup that detects existing config, prompts, and merges.
7
7
  * Supports Claude Code and Cursor IDE.
8
8
  *
9
- * Usage: npx gitmem-mcp init [--yes] [--dry-run] [--project <name>] [--client <claude|cursor>]
9
+ * Usage: npx gitmem-mcp init [--yes] [--dry-run] [--project <name>] [--client <claude|cursor|vscode|windsurf|generic>]
10
10
  */
11
11
 
12
12
  import {
@@ -35,11 +35,15 @@ const clientFlag = clientIdx !== -1 ? args[clientIdx + 1]?.toLowerCase() : null;
35
35
 
36
36
  // ── Client Configuration ──
37
37
 
38
+ // Resolve user home directory for clients that use user-level config
39
+ const homeDir = process.env.HOME || process.env.USERPROFILE || "~";
40
+
38
41
  const CLIENT_CONFIGS = {
39
42
  claude: {
40
43
  name: "Claude Code",
41
44
  mcpConfigPath: join(cwd, ".mcp.json"),
42
45
  mcpConfigName: ".mcp.json",
46
+ mcpConfigScope: "project",
43
47
  instructionsFile: join(cwd, "CLAUDE.md"),
44
48
  instructionsName: "CLAUDE.md",
45
49
  templateFile: join(__dirname, "..", "CLAUDE.md.template"),
@@ -50,12 +54,14 @@ const CLIENT_CONFIGS = {
50
54
  settingsLocalFile: join(cwd, ".claude", "settings.local.json"),
51
55
  hasPermissions: true,
52
56
  hooksInSettings: true,
57
+ hasHooks: true,
53
58
  completionMsg: "Setup complete! Start Claude Code \u2014 memory is active.",
54
59
  },
55
60
  cursor: {
56
61
  name: "Cursor",
57
62
  mcpConfigPath: join(cwd, ".cursor", "mcp.json"),
58
63
  mcpConfigName: ".cursor/mcp.json",
64
+ mcpConfigScope: "project",
59
65
  instructionsFile: join(cwd, ".cursorrules"),
60
66
  instructionsName: ".cursorrules",
61
67
  templateFile: join(__dirname, "..", "cursorrules.template"),
@@ -66,10 +72,66 @@ const CLIENT_CONFIGS = {
66
72
  settingsLocalFile: null,
67
73
  hasPermissions: false,
68
74
  hooksInSettings: false,
75
+ hasHooks: true,
69
76
  hooksFile: join(cwd, ".cursor", "hooks.json"),
70
77
  hooksFileName: ".cursor/hooks.json",
71
78
  completionMsg: "Setup complete! Open Cursor (Agent mode) \u2014 memory is active.",
72
79
  },
80
+ vscode: {
81
+ name: "VS Code (Copilot)",
82
+ mcpConfigPath: join(cwd, ".vscode", "mcp.json"),
83
+ mcpConfigName: ".vscode/mcp.json",
84
+ mcpConfigScope: "project",
85
+ instructionsFile: join(cwd, ".github", "copilot-instructions.md"),
86
+ instructionsName: ".github/copilot-instructions.md",
87
+ templateFile: join(__dirname, "..", "copilot-instructions.template"),
88
+ startMarker: "<!-- gitmem:start -->",
89
+ endMarker: "<!-- gitmem:end -->",
90
+ configDir: join(cwd, ".vscode"),
91
+ settingsFile: null,
92
+ settingsLocalFile: null,
93
+ hasPermissions: false,
94
+ hooksInSettings: false,
95
+ hasHooks: false,
96
+ completionMsg: "Setup complete! Open VS Code \u2014 memory is active via Copilot.",
97
+ },
98
+ windsurf: {
99
+ name: "Windsurf",
100
+ mcpConfigPath: join(homeDir, ".codeium", "windsurf", "mcp_config.json"),
101
+ mcpConfigName: "~/.codeium/windsurf/mcp_config.json",
102
+ mcpConfigScope: "user",
103
+ instructionsFile: join(cwd, ".windsurfrules"),
104
+ instructionsName: ".windsurfrules",
105
+ templateFile: join(__dirname, "..", "windsurfrules.template"),
106
+ startMarker: "# --- gitmem:start ---",
107
+ endMarker: "# --- gitmem:end ---",
108
+ configDir: null,
109
+ settingsFile: null,
110
+ settingsLocalFile: null,
111
+ hasPermissions: false,
112
+ hooksInSettings: false,
113
+ hasHooks: false,
114
+ completionMsg: "Setup complete! Open Windsurf \u2014 memory is active.",
115
+ },
116
+ generic: {
117
+ name: "Generic MCP Client",
118
+ mcpConfigPath: join(cwd, ".mcp.json"),
119
+ mcpConfigName: ".mcp.json",
120
+ mcpConfigScope: "project",
121
+ instructionsFile: join(cwd, "CLAUDE.md"),
122
+ instructionsName: "CLAUDE.md",
123
+ templateFile: join(__dirname, "..", "CLAUDE.md.template"),
124
+ startMarker: "<!-- gitmem:start -->",
125
+ endMarker: "<!-- gitmem:end -->",
126
+ configDir: null,
127
+ settingsFile: null,
128
+ settingsLocalFile: null,
129
+ hasPermissions: false,
130
+ hooksInSettings: false,
131
+ hasHooks: false,
132
+ completionMsg:
133
+ "Setup complete! Configure your MCP client to use the gitmem server from .mcp.json.",
134
+ },
73
135
  };
74
136
 
75
137
  // Shared paths (client-agnostic)
@@ -84,33 +146,49 @@ let cc; // shorthand for CLIENT_CONFIGS[client]
84
146
 
85
147
  // ── Client Detection ──
86
148
 
149
+ const VALID_CLIENTS = Object.keys(CLIENT_CONFIGS);
150
+
87
151
  function detectClient() {
88
152
  // Explicit flag takes priority
89
153
  if (clientFlag) {
90
- if (clientFlag !== "claude" && clientFlag !== "cursor") {
91
- console.error(` Error: Unknown client "${clientFlag}". Use --client claude or --client cursor.`);
154
+ if (!VALID_CLIENTS.includes(clientFlag)) {
155
+ console.error(` Error: Unknown client "${clientFlag}". Use --client ${VALID_CLIENTS.join("|")}.`);
92
156
  process.exit(1);
93
157
  }
94
158
  return clientFlag;
95
159
  }
96
160
 
97
- // Auto-detect based on directory presence
161
+ // Auto-detect based on directory/file presence
98
162
  const hasCursorDir = existsSync(join(cwd, ".cursor"));
99
163
  const hasClaudeDir = existsSync(join(cwd, ".claude"));
100
164
  const hasMcpJson = existsSync(join(cwd, ".mcp.json"));
101
165
  const hasClaudeMd = existsSync(join(cwd, "CLAUDE.md"));
102
166
  const hasCursorRules = existsSync(join(cwd, ".cursorrules"));
103
167
  const hasCursorMcp = existsSync(join(cwd, ".cursor", "mcp.json"));
168
+ const hasVscodeDir = existsSync(join(cwd, ".vscode"));
169
+ const hasVscodeMcp = existsSync(join(cwd, ".vscode", "mcp.json"));
170
+ const hasCopilotInstructions = existsSync(join(cwd, ".github", "copilot-instructions.md"));
171
+ const hasWindsurfRules = existsSync(join(cwd, ".windsurfrules"));
172
+ const hasWindsurfMcp = existsSync(
173
+ join(homeDir, ".codeium", "windsurf", "mcp_config.json")
174
+ );
104
175
 
105
176
  // Strong Cursor signals
106
177
  if (hasCursorDir && !hasClaudeDir && !hasMcpJson && !hasClaudeMd) return "cursor";
107
- if (hasCursorRules && !hasClaudeMd) return "cursor";
108
- if (hasCursorMcp && !hasMcpJson) return "cursor";
178
+ if (hasCursorRules && !hasClaudeMd && !hasCopilotInstructions) return "cursor";
179
+ if (hasCursorMcp && !hasMcpJson && !hasVscodeMcp) return "cursor";
109
180
 
110
181
  // Strong Claude signals
111
- if (hasClaudeDir && !hasCursorDir) return "claude";
112
- if (hasMcpJson && !hasCursorMcp) return "claude";
113
- if (hasClaudeMd && !hasCursorRules) return "claude";
182
+ if (hasClaudeDir && !hasCursorDir && !hasVscodeDir) return "claude";
183
+ if (hasMcpJson && !hasCursorMcp && !hasVscodeMcp) return "claude";
184
+ if (hasClaudeMd && !hasCursorRules && !hasCopilotInstructions) return "claude";
185
+
186
+ // VS Code signals
187
+ if (hasVscodeMcp && !hasMcpJson && !hasCursorMcp) return "vscode";
188
+ if (hasCopilotInstructions && !hasClaudeMd && !hasCursorRules) return "vscode";
189
+
190
+ // Windsurf signals
191
+ if (hasWindsurfRules && !hasClaudeMd && !hasCursorRules && !hasCopilotInstructions) return "windsurf";
114
192
 
115
193
  // Default to Claude Code (most common)
116
194
  return "claude";
@@ -428,6 +506,36 @@ async function stepMemoryStore() {
428
506
  }
429
507
  }
430
508
 
509
+ // Closing payload template — agents read this before writing closing-payload.json
510
+ const templatePath = join(gitmemDir, "closing-payload-template.json");
511
+ if (!existsSync(templatePath)) {
512
+ writeJson(templatePath, {
513
+ closing_reflection: {
514
+ what_broke: "",
515
+ what_took_longer: "",
516
+ do_differently: "",
517
+ what_worked: "",
518
+ wrong_assumption: "",
519
+ scars_applied: [],
520
+ institutional_memory_items: "",
521
+ collaborative_dynamic: "",
522
+ rapport_notes: ""
523
+ },
524
+ task_completion: {
525
+ questions_displayed_at: "ISO-8601 timestamp",
526
+ reflection_completed_at: "ISO-8601 timestamp",
527
+ human_asked_at: "ISO-8601 timestamp",
528
+ human_response_at: "ISO-8601 timestamp",
529
+ human_response: "no corrections | actual corrections text"
530
+ },
531
+ human_corrections: "",
532
+ scars_to_record: [],
533
+ learnings_created: [],
534
+ open_threads: [],
535
+ decisions: []
536
+ });
537
+ }
538
+
431
539
  console.log(
432
540
  ` Created .gitmem/ with ${starterScars.length} starter scars` +
433
541
  (added < starterScars.length
@@ -439,6 +547,7 @@ async function stepMemoryStore() {
439
547
  async function stepMcpServer() {
440
548
  const mcpPath = cc.mcpConfigPath;
441
549
  const mcpName = cc.mcpConfigName;
550
+ const isUserLevel = cc.mcpConfigScope === "user";
442
551
 
443
552
  const existing = readJson(mcpPath);
444
553
  const hasGitmem =
@@ -453,9 +562,10 @@ async function stepMcpServer() {
453
562
  ? Object.keys(existing.mcpServers).length
454
563
  : 0;
455
564
  const tierLabel = process.env.SUPABASE_URL ? "pro" : "free";
565
+ const scopeNote = isUserLevel ? " (user-level config)" : "";
456
566
  const prompt = existing
457
- ? `Add gitmem to ${mcpName}? (${serverCount} existing server${serverCount !== 1 ? "s" : ""} preserved)`
458
- : `Create ${mcpName} with gitmem server?`;
567
+ ? `Add gitmem to ${mcpName}?${scopeNote} (${serverCount} existing server${serverCount !== 1 ? "s" : ""} preserved)`
568
+ : `Create ${mcpName} with gitmem server?${scopeNote}`;
459
569
 
460
570
  if (!(await confirm(prompt))) {
461
571
  console.log(" Skipped.");
@@ -463,11 +573,11 @@ async function stepMcpServer() {
463
573
  }
464
574
 
465
575
  if (dryRun) {
466
- console.log(` [dry-run] Would add gitmem entry to ${mcpName} (${tierLabel} tier)`);
576
+ console.log(` [dry-run] Would add gitmem entry to ${mcpName} (${tierLabel} tier${scopeNote})`);
467
577
  return;
468
578
  }
469
579
 
470
- // Ensure parent directory exists (for .cursor/mcp.json)
580
+ // Ensure parent directory exists (for .cursor/mcp.json, .vscode/mcp.json, ~/.codeium/windsurf/)
471
581
  const parentDir = dirname(mcpPath);
472
582
  if (!existsSync(parentDir)) {
473
583
  mkdirSync(parentDir, { recursive: true });
@@ -481,7 +591,8 @@ async function stepMcpServer() {
481
591
  console.log(
482
592
  ` Added gitmem entry to ${mcpName} (${tierLabel} tier` +
483
593
  (process.env.SUPABASE_URL ? " \u2014 Supabase detected" : " \u2014 local storage") +
484
- ")"
594
+ ")" +
595
+ (isUserLevel ? " [user-level]" : "")
485
596
  );
486
597
  }
487
598
 
@@ -525,6 +636,12 @@ async function stepInstructions() {
525
636
  block = `${cc.startMarker}\n${block}\n${cc.endMarker}`;
526
637
  }
527
638
 
639
+ // Ensure parent directory exists (for .github/copilot-instructions.md)
640
+ const instrParentDir = dirname(instrPath);
641
+ if (!existsSync(instrParentDir)) {
642
+ mkdirSync(instrParentDir, { recursive: true });
643
+ }
644
+
528
645
  if (exists) {
529
646
  content = content.trimEnd() + "\n\n" + block + "\n";
530
647
  } else {
@@ -598,6 +715,11 @@ function copyHookScripts() {
598
715
  }
599
716
 
600
717
  async function stepHooks() {
718
+ if (!cc.hasHooks) {
719
+ console.log(` ${cc.name} does not support lifecycle hooks. Skipping.`);
720
+ console.log(" Enforcement relies on system prompt instructions instead.");
721
+ return;
722
+ }
601
723
  if (cc.hooksInSettings) {
602
724
  return stepHooksClaude();
603
725
  }
@@ -821,7 +943,7 @@ async function main() {
821
943
  );
822
944
  }
823
945
 
824
- if (!cc.hooksInSettings && cc.hooksFile && existsSync(cc.hooksFile)) {
946
+ if (!cc.hooksInSettings && cc.hasHooks && cc.hooksFile && existsSync(cc.hooksFile)) {
825
947
  const hooks = readJson(cc.hooksFile);
826
948
  const hookCount = hooks?.hooks
827
949
  ? Object.values(hooks.hooks).flat().length
@@ -850,8 +972,10 @@ async function main() {
850
972
  );
851
973
  console.log("");
852
974
 
853
- // Run steps — step count depends on client
854
- const stepCount = cc.hasPermissions ? 6 : 5;
975
+ // Run steps — step count depends on client capabilities
976
+ let stepCount = 4; // memory store + mcp server + instructions + gitignore
977
+ if (cc.hasPermissions) stepCount++;
978
+ if (cc.hasHooks) stepCount++;
855
979
  let step = 1;
856
980
 
857
981
  console.log(` Step ${step}/${stepCount} \u2014 Memory Store`);
@@ -876,10 +1000,12 @@ async function main() {
876
1000
  step++;
877
1001
  }
878
1002
 
879
- console.log(` Step ${step}/${stepCount} \u2014 Lifecycle Hooks`);
880
- await stepHooks();
881
- console.log("");
882
- step++;
1003
+ if (cc.hasHooks) {
1004
+ console.log(` Step ${step}/${stepCount} \u2014 Lifecycle Hooks`);
1005
+ await stepHooks();
1006
+ console.log("");
1007
+ step++;
1008
+ }
883
1009
 
884
1010
  console.log(` Step ${step}/${stepCount} \u2014 Gitignore`);
885
1011
  await stepGitignore();
@@ -0,0 +1,82 @@
1
+ <!-- gitmem:start -->
2
+ # GitMem — Persistent Memory
3
+
4
+ You have a persistent memory layer called gitmem. Every session starts from
5
+ zero — but gitmem carries forward lessons, mistakes, wins, decisions, and
6
+ open threads across sessions and projects. Search it. Test it. You might be
7
+ amazed at what's already there. If convinced, make it your own.
8
+
9
+ ## Red Lines
10
+
11
+ ### Credential Protection
12
+
13
+ Credential exposure in conversation history is permanent and irreversible.
14
+
15
+ 1. **NEVER read credential files in full.** Files like `mcp-config.json`, `.env`, `.credentials.json`, `.netrc`, `.npmrc`, SSH keys, or `.pem`/`.key` files must not be read or dumped.
16
+ 2. **NEVER print environment variable values that contain secrets.** Commands like `env | grep KEY`, `echo $API_KEY`, or `printenv TOKEN` expose credentials in output.
17
+ 3. **NEVER display API keys, tokens, or secrets in conversation output.**
18
+
19
+ Safe alternatives: `env | grep -c VARNAME` (count only), `[ -n "$VARNAME" ] && echo "set"` (existence check), `grep -c '"key"' config.json` (structure check).
20
+
21
+ ### Recall Before Consequential Actions
22
+
23
+ 1. **NEVER parallelize `recall()` with actions that expose, modify, or transmit sensitive data.** Recall must complete first.
24
+ 2. **Confirm scars before acting.** Each recalled scar requires APPLYING (past-tense evidence), N_A (explanation), or REFUTED (risk acknowledgment).
25
+ 3. **Parallel recall is only safe with benign reads** — source code, docs, non-sensitive config.
26
+
27
+ ## Tools
28
+
29
+ | Tool | When to use |
30
+ |------|-------------|
31
+ | `recall` | Before any task — surfaces relevant warnings from past experience |
32
+ | `confirm_scars` | After recall — acknowledge each scar as APPLYING, N_A, or REFUTED |
33
+ | `search` | Explore institutional knowledge by topic |
34
+ | `log` | Browse recent learnings chronologically |
35
+ | `session_start` | Beginning of session — loads last session context and open threads |
36
+ | `session_close` | End of session — persists what you learned |
37
+ | `create_learning` | Capture a mistake (scar), success (win), or pattern |
38
+ | `create_decision` | Log an architectural or operational decision with rationale |
39
+ | `list_threads` | See unresolved work carrying over between sessions |
40
+ | `create_thread` | Track something that needs follow-up in a future session |
41
+ | `help` | Show all available commands |
42
+
43
+ ## Session Lifecycle
44
+
45
+ **Start:** Call `session_start()` at the beginning of every session.
46
+
47
+ **End:** On "closing", "done for now", or "wrapping up":
48
+
49
+ 1. **Answer these reflection questions** and display to the human:
50
+ - What broke that you didn't expect?
51
+ - What took longer than it should have?
52
+ - What would you do differently next time?
53
+ - What pattern or approach worked well?
54
+ - What assumption was wrong?
55
+ - Which scars did you apply?
56
+ - What should be captured as institutional memory?
57
+
58
+ 2. **Ask the human**: "Any corrections or additions?" Wait for their response.
59
+
60
+ 3. **Write payload** to `.gitmem/closing-payload.json`:
61
+ ```json
62
+ {
63
+ "closing_reflection": {
64
+ "what_broke": "...",
65
+ "what_took_longer": "...",
66
+ "do_differently": "...",
67
+ "what_worked": "...",
68
+ "wrong_assumption": "...",
69
+ "scars_applied": ["scar title 1", "scar title 2"],
70
+ "institutional_memory_items": "..."
71
+ },
72
+ "human_corrections": "",
73
+ "scars_to_record": [],
74
+ "open_threads": [],
75
+ "decisions": []
76
+ }
77
+ ```
78
+
79
+ 4. **Call `session_close`** with `session_id` and `close_type: "standard"`
80
+
81
+ For short exploratory sessions (< 30 min, no real work), use `close_type: "quick"` — no questions needed.
82
+ <!-- gitmem:end -->
@@ -279,16 +279,6 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
279
279
  }[] | undefined;
280
280
  transcript_path?: string | undefined;
281
281
  linear_issue?: string | undefined;
282
- open_threads?: (string | {
283
- id: string;
284
- created_at: string;
285
- status: "open" | "resolved";
286
- text: string;
287
- resolved_at?: string | undefined;
288
- resolution_note?: string | undefined;
289
- source_session?: string | undefined;
290
- resolved_by_session?: string | undefined;
291
- })[] | undefined;
292
282
  closing_reflection?: {
293
283
  what_broke: string;
294
284
  what_took_longer: string;
@@ -300,7 +290,6 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
300
290
  collaborative_dynamic?: string | undefined;
301
291
  rapport_notes?: string | undefined;
302
292
  } | undefined;
303
- project_state?: string | undefined;
304
293
  task_completion?: {
305
294
  questions_displayed_at: string;
306
295
  reflection_completed_at: string;
@@ -309,8 +298,6 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
309
298
  human_response: string;
310
299
  } | undefined;
311
300
  human_corrections?: string | undefined;
312
- learnings_created?: (string | Record<string, unknown>)[] | undefined;
313
- ceremony_duration_ms?: number | undefined;
314
301
  scars_to_record?: {
315
302
  surfaced_at: string;
316
303
  reference_type: "explicit" | "implicit" | "acknowledged" | "refuted" | "none";
@@ -324,6 +311,19 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
324
311
  execution_successful?: boolean | undefined;
325
312
  variant_id?: string | undefined;
326
313
  }[] | undefined;
314
+ learnings_created?: (string | Record<string, unknown>)[] | undefined;
315
+ open_threads?: (string | {
316
+ id: string;
317
+ created_at: string;
318
+ status: "open" | "resolved";
319
+ text: string;
320
+ resolved_at?: string | undefined;
321
+ resolution_note?: string | undefined;
322
+ source_session?: string | undefined;
323
+ resolved_by_session?: string | undefined;
324
+ })[] | undefined;
325
+ project_state?: string | undefined;
326
+ ceremony_duration_ms?: number | undefined;
327
327
  capture_transcript?: boolean | undefined;
328
328
  }, {
329
329
  session_id: string;
@@ -336,16 +336,6 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
336
336
  }[] | undefined;
337
337
  transcript_path?: string | undefined;
338
338
  linear_issue?: string | undefined;
339
- open_threads?: (string | {
340
- id: string;
341
- created_at: string;
342
- status: "open" | "resolved";
343
- text: string;
344
- resolved_at?: string | undefined;
345
- resolution_note?: string | undefined;
346
- source_session?: string | undefined;
347
- resolved_by_session?: string | undefined;
348
- })[] | undefined;
349
339
  closing_reflection?: {
350
340
  what_broke: string;
351
341
  what_took_longer: string;
@@ -357,7 +347,6 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
357
347
  collaborative_dynamic?: string | undefined;
358
348
  rapport_notes?: string | undefined;
359
349
  } | undefined;
360
- project_state?: string | undefined;
361
350
  task_completion?: {
362
351
  questions_displayed_at: string;
363
352
  reflection_completed_at: string;
@@ -366,8 +355,6 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
366
355
  human_response: string;
367
356
  } | undefined;
368
357
  human_corrections?: string | undefined;
369
- learnings_created?: (string | Record<string, unknown>)[] | undefined;
370
- ceremony_duration_ms?: number | undefined;
371
358
  scars_to_record?: {
372
359
  surfaced_at: string;
373
360
  reference_type: "explicit" | "implicit" | "acknowledged" | "refuted" | "none";
@@ -381,6 +368,19 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
381
368
  execution_successful?: boolean | undefined;
382
369
  variant_id?: string | undefined;
383
370
  }[] | undefined;
371
+ learnings_created?: (string | Record<string, unknown>)[] | undefined;
372
+ open_threads?: (string | {
373
+ id: string;
374
+ created_at: string;
375
+ status: "open" | "resolved";
376
+ text: string;
377
+ resolved_at?: string | undefined;
378
+ resolution_note?: string | undefined;
379
+ source_session?: string | undefined;
380
+ resolved_by_session?: string | undefined;
381
+ })[] | undefined;
382
+ project_state?: string | undefined;
383
+ ceremony_duration_ms?: number | undefined;
384
384
  capture_transcript?: boolean | undefined;
385
385
  }>;
386
386
  export type SessionCloseParams = z.infer<typeof SessionCloseParamsSchema>;
package/dist/server.js CHANGED
@@ -4,6 +4,9 @@
4
4
  * Registers all tools and handles MCP protocol communication.
5
5
  * Tool definitions are in ./tools/definitions.ts
6
6
  */
7
+ import { createRequire } from "module";
8
+ const require = createRequire(import.meta.url);
9
+ const pkg = require("../package.json");
7
10
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
11
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
12
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
@@ -34,6 +37,7 @@ import { archiveLearning } from "./tools/archive-learning.js";
34
37
  import { getCacheStatus, checkCacheHealth, flushCache, startBackgroundInit, } from "./services/startup.js";
35
38
  import { getEffectTracker } from "./services/effect-tracker.js";
36
39
  import { getProject } from "./services/session-state.js";
40
+ import { checkEnforcement } from "./services/enforcement.js";
37
41
  import { getTier, hasSupabase, hasCacheManagement, hasBatchOperations, hasTranscripts, } from "./services/tier.js";
38
42
  import { getRegisteredTools } from "./tools/definitions.js";
39
43
  import { validateToolArgs } from "./schemas/registry.js";
@@ -43,7 +47,7 @@ import { validateToolArgs } from "./schemas/registry.js";
43
47
  export function createServer() {
44
48
  const server = new Server({
45
49
  name: "gitmem-mcp",
46
- version: "1.0.3",
50
+ version: pkg.version,
47
51
  }, {
48
52
  capabilities: {
49
53
  tools: {},
@@ -84,6 +88,8 @@ export function createServer() {
84
88
  isError: true,
85
89
  };
86
90
  }
91
+ // Server-side enforcement: advisory warnings for protocol violations
92
+ const enforcement = checkEnforcement(name);
87
93
  try {
88
94
  let result;
89
95
  switch (name) {
@@ -240,7 +246,7 @@ export function createServer() {
240
246
  // Build command table for display
241
247
  const cmdLines = visibleCommands.map(c => ` ${c.alias.padEnd(22)} ${c.description}`).join("\n");
242
248
  const display = [
243
- `gitmem v1.0.3 · ${tier} · ${registeredTools.length} tools · ${hasSupabase() ? "supabase" : "local (.gitmem/)"}`,
249
+ `gitmem v${pkg.version} · ${tier} · ${registeredTools.length} tools · ${hasSupabase() ? "supabase" : "local (.gitmem/)"}`,
244
250
  "Memory that compounds.",
245
251
  "",
246
252
  cmdLines,
@@ -252,7 +258,7 @@ export function createServer() {
252
258
  "Tool results are collapsed in the CLI — the user cannot see them unless you echo them.",
253
259
  ].join("\n");
254
260
  result = {
255
- version: "1.0.3",
261
+ version: pkg.version,
256
262
  tier,
257
263
  tools_registered: registeredTools.length,
258
264
  storage: hasSupabase() ? "supabase" : "local (.gitmem/)",
@@ -313,6 +319,10 @@ export function createServer() {
313
319
  else {
314
320
  responseText = JSON.stringify(result, null, 2);
315
321
  }
322
+ // Prepend enforcement warning if present (advisory, non-blocking)
323
+ if (enforcement.warning) {
324
+ responseText = enforcement.warning + "\n\n" + responseText;
325
+ }
316
326
  return {
317
327
  content: [
318
328
  {
@@ -365,7 +365,7 @@ export function formatSummary(data) {
365
365
  const lines = [
366
366
  `## ${period.days}-Day Summary (${period.start} to ${period.end})`,
367
367
  ``,
368
- `**Sessions:** ${total_sessions} | **Decisions:** ${total_decisions} | **Open Threads:** ${total_open_threads}`,
368
+ `**Sessions:** ${total_sessions} | **Decisions:** ${total_decisions} | **Threads Referenced:** ${total_open_threads}`,
369
369
  `**With Reflections:** ${sessions_with_reflections} | **With Issues:** ${sessions_with_issues}`,
370
370
  ``,
371
371
  `### Agents`,
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Server-Side Enforcement Layer
3
+ *
4
+ * Advisory warnings that surface in tool responses when the agent
5
+ * hasn't followed the recall → confirm → act protocol.
6
+ *
7
+ * Design principles:
8
+ * - Advisory, not blocking: warnings append to responses, never prevent execution
9
+ * - Zero overhead on compliant calls: only fires when state is missing
10
+ * - Universal: works in ALL MCP clients, no IDE hooks needed
11
+ * - Lightweight: pure in-memory checks, no I/O
12
+ */
13
+ export interface EnforcementResult {
14
+ /** Warning text to prepend to tool response (null = clean, no warning) */
15
+ warning: string | null;
16
+ }
17
+ /**
18
+ * Run pre-dispatch enforcement checks for a tool call.
19
+ *
20
+ * Returns a warning string to prepend to the response, or null if clean.
21
+ * Never blocks execution — always advisory.
22
+ */
23
+ export declare function checkEnforcement(toolName: string): EnforcementResult;
24
+ //# sourceMappingURL=enforcement.d.ts.map
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Server-Side Enforcement Layer
3
+ *
4
+ * Advisory warnings that surface in tool responses when the agent
5
+ * hasn't followed the recall → confirm → act protocol.
6
+ *
7
+ * Design principles:
8
+ * - Advisory, not blocking: warnings append to responses, never prevent execution
9
+ * - Zero overhead on compliant calls: only fires when state is missing
10
+ * - Universal: works in ALL MCP clients, no IDE hooks needed
11
+ * - Lightweight: pure in-memory checks, no I/O
12
+ */
13
+ import { getCurrentSession, hasUnconfirmedScars, getSurfacedScars } from "./session-state.js";
14
+ /**
15
+ * Tools that require an active session to function properly.
16
+ * Read-only/administrative tools are excluded.
17
+ */
18
+ const SESSION_REQUIRED_TOOLS = new Set([
19
+ "recall", "gitmem-r",
20
+ "confirm_scars", "gitmem-cs", "gm-confirm",
21
+ "session_close", "gitmem-sc", "gm-close",
22
+ "session_refresh", "gitmem-sr", "gm-refresh",
23
+ "create_learning", "gitmem-cl", "gm-scar",
24
+ "create_decision", "gitmem-cd",
25
+ "record_scar_usage", "gitmem-rs",
26
+ "record_scar_usage_batch", "gitmem-rsb",
27
+ "prepare_context", "gitmem-pc", "gm-pc",
28
+ "absorb_observations", "gitmem-ao", "gm-absorb",
29
+ "create_thread", "gitmem-ct", "gm-thread-new",
30
+ "resolve_thread", "gitmem-rt", "gm-resolve",
31
+ "save_transcript", "gitmem-st",
32
+ ]);
33
+ /**
34
+ * Tools that represent "consequential actions" — the agent is creating
35
+ * or modifying state. These should ideally happen after recall + confirm.
36
+ */
37
+ const CONSEQUENTIAL_TOOLS = new Set([
38
+ "create_learning", "gitmem-cl", "gm-scar",
39
+ "create_decision", "gitmem-cd",
40
+ "create_thread", "gitmem-ct", "gm-thread-new",
41
+ "session_close", "gitmem-sc", "gm-close",
42
+ ]);
43
+ /**
44
+ * Tools that are always safe — no enforcement checks needed.
45
+ * Includes session_start (which creates the session), read-only tools,
46
+ * and administrative tools.
47
+ */
48
+ const EXEMPT_TOOLS = new Set([
49
+ "session_start", "gitmem-ss", "gm-open",
50
+ "search", "gitmem-search", "gm-search",
51
+ "log", "gitmem-log", "gm-log",
52
+ "analyze", "gitmem-analyze", "gm-analyze",
53
+ "graph_traverse", "gitmem-graph", "gm-graph",
54
+ "list_threads", "gitmem-lt", "gm-threads",
55
+ "cleanup_threads", "gitmem-cleanup", "gm-cleanup",
56
+ "promote_suggestion", "gitmem-ps", "gm-promote",
57
+ "dismiss_suggestion", "gitmem-ds", "gm-dismiss",
58
+ "archive_learning", "gitmem-al", "gm-archive",
59
+ "get_transcript", "gitmem-gt",
60
+ "search_transcripts", "gitmem-stx", "gm-stx",
61
+ "gitmem-help",
62
+ "health", "gitmem-health", "gm-health",
63
+ "gitmem-cache-status", "gm-cache-s",
64
+ "gitmem-cache-health", "gm-cache-h",
65
+ "gitmem-cache-flush", "gm-cache-f",
66
+ ]);
67
+ /**
68
+ * Run pre-dispatch enforcement checks for a tool call.
69
+ *
70
+ * Returns a warning string to prepend to the response, or null if clean.
71
+ * Never blocks execution — always advisory.
72
+ */
73
+ export function checkEnforcement(toolName) {
74
+ // Exempt tools skip all checks
75
+ if (EXEMPT_TOOLS.has(toolName)) {
76
+ return { warning: null };
77
+ }
78
+ const session = getCurrentSession();
79
+ // Check 1: No active session
80
+ if (!session && SESSION_REQUIRED_TOOLS.has(toolName)) {
81
+ return {
82
+ warning: [
83
+ "--- gitmem enforcement ---",
84
+ "No active session. Call session_start() first to initialize memory context.",
85
+ "Without a session, scars won't be tracked and the closing ceremony can't run.",
86
+ "---",
87
+ ].join("\n"),
88
+ };
89
+ }
90
+ // If no session and not session-required, skip remaining checks
91
+ if (!session) {
92
+ return { warning: null };
93
+ }
94
+ // Check 2: Unconfirmed scars before consequential action
95
+ if (CONSEQUENTIAL_TOOLS.has(toolName) && hasUnconfirmedScars()) {
96
+ const recallScars = getSurfacedScars().filter(s => s.source === "recall");
97
+ const confirmedCount = session.confirmations?.length || 0;
98
+ const pendingCount = recallScars.length - confirmedCount;
99
+ return {
100
+ warning: [
101
+ "--- gitmem enforcement ---",
102
+ `${pendingCount} recalled scar(s) await confirmation.`,
103
+ "Call confirm_scars() with APPLYING/N_A/REFUTED for each before proceeding.",
104
+ "Unconfirmed scars may contain warnings relevant to what you're about to do.",
105
+ "---",
106
+ ].join("\n"),
107
+ };
108
+ }
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
+ }
123
+ }
124
+ return { warning: null };
125
+ }
126
+ //# sourceMappingURL=enforcement.js.map
@@ -44,12 +44,16 @@ function buildCleanupDisplay(summary, groups, archivedCount) {
44
44
  for (const [label, items] of sections) {
45
45
  if (items.length === 0)
46
46
  continue;
47
- lines.push(`${label} (${items.length}):`);
47
+ lines.push(`**${label}** (${items.length}):`);
48
+ lines.push("");
49
+ lines.push("| ID | Thread | Last Touch |");
50
+ lines.push("|----|--------|------------|");
48
51
  for (const t of items) {
49
- const text = truncate(t.text, 48);
52
+ const text = truncate(t.text, 60);
50
53
  const time = formatDaysAgo(t.days_since_touch);
51
- lines.push(` ${t.thread_id} ${text.padEnd(50)} ${time.padStart(8)}`);
54
+ lines.push(`| ${t.thread_id} | ${text} | ${time} |`);
52
55
  }
56
+ lines.push("");
53
57
  }
54
58
  lines.push("");
55
59
  lines.push(`Archived: ${archivedCount}`);
@@ -158,7 +158,7 @@ export const TOOLS = [
158
158
  },
159
159
  {
160
160
  name: "create_learning",
161
- description: "Create scar, win, or pattern entry in institutional memory",
161
+ description: "Create scar, win, or pattern entry in institutional memory. Frame as 'what we now know' — lead with the factual/architectural discovery, not what went wrong. Good: 'Fine-grained PATs are scoped to one resource owner'. Bad: 'Should have checked PAT type first'.",
162
162
  inputSchema: {
163
163
  type: "object",
164
164
  properties: {
@@ -169,11 +169,11 @@ export const TOOLS = [
169
169
  },
170
170
  title: {
171
171
  type: "string",
172
- description: "Learning title",
172
+ description: "Frame as a knowledge discovery — what we now know. Lead with the factual insight, not self-criticism.",
173
173
  },
174
174
  description: {
175
175
  type: "string",
176
- description: "Detailed description",
176
+ description: "Detailed description. Include the architectural/behavioral fact that makes this retrievable by domain.",
177
177
  },
178
178
  severity: {
179
179
  type: "string",
@@ -849,7 +849,7 @@ export const TOOLS = [
849
849
  },
850
850
  {
851
851
  name: "gitmem-cl",
852
- description: "gitmem-cl (create_learning) - Create scar/win/pattern in institutional memory",
852
+ description: "gitmem-cl (create_learning) - Create scar/win/pattern. Frame as 'what we now know' — factual discovery, not self-criticism.",
853
853
  inputSchema: {
854
854
  type: "object",
855
855
  properties: {
@@ -1496,7 +1496,7 @@ export const TOOLS = [
1496
1496
  },
1497
1497
  {
1498
1498
  name: "gm-scar",
1499
- description: "gm-scar (create_learning) - Create a scar/win/pattern in institutional memory",
1499
+ description: "gm-scar (create_learning) - Create a scar/win/pattern. Frame as 'what we now know' — factual discovery, not self-criticism.",
1500
1500
  inputSchema: {
1501
1501
  type: "object",
1502
1502
  properties: {
@@ -39,24 +39,16 @@ function buildThreadsDisplay(threads, totalOpen, totalResolved) {
39
39
  }
40
40
  // Deterministic sort: oldest first by created_at
41
41
  const sorted = [...threads].sort((a, b) => a.created_at.localeCompare(b.created_at));
42
- const NUM_W = 3;
43
- const ID_W = 12;
44
- const TEXT_W = 40;
45
- const DATE_W = 8;
46
- const hr = (l, j, r) => `${l}${"─".repeat(NUM_W + 2)}${j}${"─".repeat(ID_W + 2)}${j}${"─".repeat(TEXT_W + 2)}${j}${"─".repeat(DATE_W + 2)}${r}`;
47
- const hdr = (l, j, r) => `${l}${"═".repeat(NUM_W + 2)}${j}${"═".repeat(ID_W + 2)}${j}${"═".repeat(TEXT_W + 2)}${j}${"═".repeat(DATE_W + 2)}${r}`;
48
- const row = (n, id, t, d) => `│ ${n.padEnd(NUM_W)} │ ${id.padEnd(ID_W)} │ ${t.padEnd(TEXT_W)} │ ${d.padStart(DATE_W)} │`;
49
- lines.push(hr("┌", "┬", "┐"));
50
- lines.push(row("#", "ID", "Thread", "Active"));
51
- lines.push(hdr("╞", "╪", "╡"));
42
+ // Markdown table — renders cleanly in all MCP clients
43
+ lines.push("| # | ID | Thread | Active |");
44
+ lines.push("|---|-----|--------|--------|");
52
45
  for (let i = 0; i < sorted.length; i++) {
53
46
  const t = sorted[i];
54
- const shortId = truncate(t.id, ID_W);
55
- const text = truncate(t.text, TEXT_W);
47
+ const shortId = t.id;
48
+ const text = truncate(t.text, 60);
56
49
  const date = shortDate(t.last_touched_at || t.created_at);
57
- lines.push(row(`${i + 1}.`, shortId, text, date));
50
+ lines.push(`| ${i + 1} | ${shortId} | ${text} | ${date} |`);
58
51
  }
59
- lines.push(hr("└", "┴", "┘"));
60
52
  return wrapDisplay(lines.join("\n"));
61
53
  }
62
54
  export async function listThreads(params) {
package/dist/tools/log.js CHANGED
@@ -20,7 +20,7 @@ import { wrapDisplay, relativeTime, truncate, SEV, TYPE } from "../services/disp
20
20
  // --- Display Formatting ---
21
21
  function buildLogDisplay(entries, total, filters) {
22
22
  const lines = [];
23
- lines.push(`gitmem log · ${total} entries · ${filters.project}`);
23
+ lines.push(`gitmem log · ${total} most recent learnings · ${filters.project}`);
24
24
  const fp = [];
25
25
  if (filters.learning_type)
26
26
  fp.push(`type=${filters.learning_type}`);
@@ -5,6 +5,8 @@
5
5
  import { v4 as uuidv4 } from "uuid";
6
6
  import * as supabase from "../services/supabase-client.js";
7
7
  import { hasSupabase, getTableName } from "../services/tier.js";
8
+ import { detectAgent } from "../services/agent-detection.js";
9
+ import { getCurrentSession } from "../services/session-state.js";
8
10
  import { Timer, recordMetrics, buildPerformanceData } from "../services/metrics.js";
9
11
  const TARGET_LATENCY_MS = 2000; // Target for batch operation
10
12
  /**
@@ -84,6 +86,9 @@ export async function recordScarUsageBatch(params) {
84
86
  return { entry, scarId };
85
87
  });
86
88
  const resolvedScars = await Promise.all(resolutionPromises);
89
+ // Auto-detect agent and session as fallbacks for entries missing them
90
+ const fallbackAgent = detectAgent().agent || null;
91
+ const fallbackSessionId = getCurrentSession()?.sessionId || null;
87
92
  // Build usage records for all successfully resolved scars
88
93
  const usageRecords = resolvedScars
89
94
  .filter(({ scarId }) => scarId !== null)
@@ -94,8 +99,8 @@ export async function recordScarUsageBatch(params) {
94
99
  scar_id: scarId,
95
100
  issue_id: entry.issue_id || null,
96
101
  issue_identifier: entry.issue_identifier || null,
97
- session_id: entry.session_id || null, // Session tracking
98
- agent: entry.agent || null, // Agent identity
102
+ session_id: entry.session_id || fallbackSessionId,
103
+ agent: entry.agent || fallbackAgent,
99
104
  surfaced_at: entry.surfaced_at,
100
105
  acknowledged_at: entry.acknowledged_at || null,
101
106
  referenced: entry.reference_type !== "none",
@@ -11,6 +11,8 @@ import { wrapDisplay } from "../services/display-protocol.js";
11
11
  import * as supabase from "../services/supabase-client.js";
12
12
  import { hasSupabase } from "../services/tier.js";
13
13
  import { getStorage } from "../services/storage.js";
14
+ import { detectAgent } from "../services/agent-detection.js";
15
+ import { getCurrentSession } from "../services/session-state.js";
14
16
  import { Timer, recordMetrics, buildPerformanceData, } from "../services/metrics.js";
15
17
  /**
16
18
  * Execute record_scar_usage tool
@@ -19,13 +21,16 @@ export async function recordScarUsage(params) {
19
21
  const timer = new Timer();
20
22
  const metricsId = uuidv4();
21
23
  const usageId = uuidv4();
24
+ // Auto-detect agent and session if not provided by caller
25
+ const resolvedAgent = params.agent || detectAgent().agent || null;
26
+ const resolvedSessionId = params.session_id || getCurrentSession()?.sessionId || null;
22
27
  const usageData = {
23
28
  id: usageId,
24
29
  scar_id: params.scar_id,
25
30
  issue_id: params.issue_id || null,
26
31
  issue_identifier: params.issue_identifier || null,
27
- session_id: params.session_id || null, // Session tracking
28
- agent: params.agent || null, // Agent identity
32
+ session_id: resolvedSessionId,
33
+ agent: resolvedAgent,
29
34
  surfaced_at: params.surfaced_at,
30
35
  acknowledged_at: params.acknowledged_at || null,
31
36
  referenced: params.reference_type !== "none",
@@ -28,6 +28,33 @@ import { setGitmemDir, getGitmemDir, getSessionPath, getConfigProject } from "..
28
28
  import { registerSession, findSessionByHostPid, pruneStale, migrateFromLegacy } from "../services/active-sessions.js";
29
29
  import * as os from "os";
30
30
  import { formatDate } from "../services/timezone.js";
31
+ /**
32
+ * Closing payload schema — returned in session_start/refresh so agents
33
+ * know the exact field names for closing-payload.json without guessing.
34
+ */
35
+ const CLOSING_PAYLOAD_SCHEMA = {
36
+ closing_reflection: {
37
+ what_broke: "Q1: What broke that you didn't expect?",
38
+ what_took_longer: "Q2: What took longer than it should have?",
39
+ do_differently: "Q3: What would you do differently next time?",
40
+ what_worked: "Q4: What pattern or approach worked well?",
41
+ wrong_assumption: "Q5: What assumption was wrong?",
42
+ scars_applied: ["Q6: scar titles applied"],
43
+ institutional_memory_items: "Q7: What to capture as institutional memory?",
44
+ },
45
+ task_completion: {
46
+ questions_displayed_at: "ISO-8601",
47
+ reflection_completed_at: "ISO-8601",
48
+ human_asked_at: "ISO-8601",
49
+ human_response_at: "ISO-8601",
50
+ human_response: "human's corrections or 'no corrections'",
51
+ },
52
+ human_corrections: "",
53
+ scars_to_record: [],
54
+ learnings_created: [],
55
+ open_threads: [],
56
+ decisions: [],
57
+ };
31
58
  /**
32
59
  * Normalize decisions from mixed formats (strings or objects) to string[].
33
60
  * Historical sessions (pre-2026) stored {title, decision} objects.
@@ -413,6 +440,7 @@ async function sessionStartFree(params, env, agent, project, timer, metricsId, e
413
440
  ...(freeMergedThreads.length > 0 && { open_threads: freeMergedThreads }),
414
441
  recent_decisions: decisions,
415
442
  gitmem_dir: getGitmemDir(),
443
+ closing_payload_schema: CLOSING_PAYLOAD_SCHEMA,
416
444
  project,
417
445
  performance,
418
446
  };
@@ -573,13 +601,27 @@ function writeSessionFiles(sessionId, agent, project, surfacedScars, threads, re
573
601
  const existingFileThreads = loadThreadsFile();
574
602
  let merged;
575
603
  if (supabaseAuthoritative) {
576
- // Supabase is source of truth — use its threads, but preserve any local-only threads
577
- // (threads in the file that don't exist in the Supabase set, e.g. created via create_thread
578
- // mid-session but not yet synced to Supabase by session_close).
604
+ // Supabase is source of truth — use its threads, but preserve local-only threads
605
+ // that were created recently (within 24h) and have a valid ID. These are likely
606
+ // mid-session threads not yet synced by session_close. Older local-only threads
607
+ // are stale — they were resolved/archived in Supabase but linger in the file
608
+ // because the NOT-IN query excludes them, making them look "local-only".
579
609
  const supabaseIds = new Set(threads.map(t => t.id));
580
- const localOnlyThreads = existingFileThreads.filter(t => !supabaseIds.has(t.id));
610
+ const cutoff = Date.now() - 24 * 60 * 60 * 1000;
611
+ const localOnlyThreads = existingFileThreads.filter(t => {
612
+ if (supabaseIds.has(t.id))
613
+ return false; // exists in Supabase active set
614
+ if (!t.id)
615
+ return false; // no ID = malformed, drop
616
+ const created = t.created_at ? new Date(t.created_at).getTime() : 0;
617
+ return created > cutoff; // only keep if created within last 24h
618
+ });
619
+ const dropped = existingFileThreads.filter(t => !supabaseIds.has(t.id)).length - localOnlyThreads.length;
581
620
  if (localOnlyThreads.length > 0) {
582
- console.error(`[session_start] Preserving ${localOnlyThreads.length} local-only threads not yet in Supabase`);
621
+ console.error(`[session_start] Preserving ${localOnlyThreads.length} recent local-only threads not yet in Supabase`);
622
+ }
623
+ if (dropped > 0) {
624
+ console.error(`[session_start] Dropped ${dropped} stale local-only threads (resolved/archived in Supabase)`);
583
625
  }
584
626
  merged = deduplicateThreadList([...threads, ...localOnlyThreads]);
585
627
  }
@@ -809,6 +851,7 @@ export async function sessionStart(params) {
809
851
  recent_decisions: decisions,
810
852
  ...(recordingPath && { recording_path: recordingPath }),
811
853
  gitmem_dir: getGitmemDir(),
854
+ closing_payload_schema: CLOSING_PAYLOAD_SCHEMA,
812
855
  project,
813
856
  performance,
814
857
  };
@@ -931,6 +974,7 @@ export async function sessionRefresh(params) {
931
974
  recent_decisions: decisions,
932
975
  // Rapport disabled — not injected into session context
933
976
  ...(recordingPath && { recording_path: recordingPath }),
977
+ closing_payload_schema: CLOSING_PAYLOAD_SCHEMA,
934
978
  project,
935
979
  performance,
936
980
  };
@@ -163,6 +163,8 @@ export interface SessionStartResult {
163
163
  recording_path?: string;
164
164
  /** Resolved .gitmem directory path — use for closing-payload.json writes */
165
165
  gitmem_dir?: string;
166
+ /** Closing payload schema — exact field names for closing-payload.json */
167
+ closing_payload_schema?: Record<string, unknown>;
166
168
  /** Project namespace for this session */
167
169
  project?: string;
168
170
  /** Pre-formatted display string for consistent CLI output */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitmem-mcp",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "Institutional memory for AI coding agents. Memory that compounds.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -40,7 +40,7 @@
40
40
  "@types/uuid": "^9.0.0",
41
41
  "pg": "^8.11.0",
42
42
  "typescript": "^5.0.0",
43
- "vitest": "^1.0.0"
43
+ "vitest": "^4.0.18"
44
44
  },
45
45
  "engines": {
46
46
  "node": ">=18.0.0"
@@ -55,16 +55,26 @@
55
55
  "schema",
56
56
  "CLAUDE.md.template",
57
57
  "cursorrules.template",
58
+ "copilot-instructions.template",
59
+ "windsurfrules.template",
58
60
  "README.md",
59
61
  "CHANGELOG.md"
60
62
  ],
61
63
  "keywords": [
62
64
  "mcp",
65
+ "mcp-server",
63
66
  "gitmem",
64
67
  "claude",
68
+ "claude-code",
65
69
  "cursor",
66
- "institutional-memory",
70
+ "vscode",
71
+ "copilot",
72
+ "windsurf",
73
+ "openclaw",
74
+ "ai-memory",
75
+ "ai-agent",
67
76
  "ai-coding",
77
+ "institutional-memory",
68
78
  "scars",
69
79
  "semantic-search"
70
80
  ],
@@ -0,0 +1,82 @@
1
+ # --- gitmem:start ---
2
+ # GitMem — Persistent Memory
3
+
4
+ You have a persistent memory layer called gitmem. Every session starts from
5
+ zero — but gitmem carries forward lessons, mistakes, wins, decisions, and
6
+ open threads across sessions and projects. Search it. Test it. You might be
7
+ amazed at what's already there. If convinced, make it your own.
8
+
9
+ ## Red Lines
10
+
11
+ ### Credential Protection
12
+
13
+ Credential exposure in conversation history is permanent and irreversible.
14
+
15
+ 1. **NEVER read credential files in full.** Files like `mcp-config.json`, `.env`, `.credentials.json`, `.netrc`, `.npmrc`, SSH keys, or `.pem`/`.key` files must not be read or dumped.
16
+ 2. **NEVER print environment variable values that contain secrets.** Commands like `env | grep KEY`, `echo $API_KEY`, or `printenv TOKEN` expose credentials in output.
17
+ 3. **NEVER display API keys, tokens, or secrets in conversation output.**
18
+
19
+ Safe alternatives: `env | grep -c VARNAME` (count only), `[ -n "$VARNAME" ] && echo "set"` (existence check), `grep -c '"key"' config.json` (structure check).
20
+
21
+ ### Recall Before Consequential Actions
22
+
23
+ 1. **NEVER parallelize `recall()` with actions that expose, modify, or transmit sensitive data.** Recall must complete first.
24
+ 2. **Confirm scars before acting.** Each recalled scar requires APPLYING (past-tense evidence), N_A (explanation), or REFUTED (risk acknowledgment).
25
+ 3. **Parallel recall is only safe with benign reads** — source code, docs, non-sensitive config.
26
+
27
+ ## Tools
28
+
29
+ | Tool | When to use |
30
+ |------|-------------|
31
+ | `recall` | Before any task — surfaces relevant warnings from past experience |
32
+ | `confirm_scars` | After recall — acknowledge each scar as APPLYING, N_A, or REFUTED |
33
+ | `search` | Explore institutional knowledge by topic |
34
+ | `log` | Browse recent learnings chronologically |
35
+ | `session_start` | Beginning of session — loads last session context and open threads |
36
+ | `session_close` | End of session — persists what you learned |
37
+ | `create_learning` | Capture a mistake (scar), success (win), or pattern |
38
+ | `create_decision` | Log an architectural or operational decision with rationale |
39
+ | `list_threads` | See unresolved work carrying over between sessions |
40
+ | `create_thread` | Track something that needs follow-up in a future session |
41
+ | `help` | Show all available commands |
42
+
43
+ ## Session Lifecycle
44
+
45
+ **Start:** Call `session_start()` at the beginning of every session.
46
+
47
+ **End:** On "closing", "done for now", or "wrapping up":
48
+
49
+ 1. **Answer these reflection questions** and display to the human:
50
+ - What broke that you didn't expect?
51
+ - What took longer than it should have?
52
+ - What would you do differently next time?
53
+ - What pattern or approach worked well?
54
+ - What assumption was wrong?
55
+ - Which scars did you apply?
56
+ - What should be captured as institutional memory?
57
+
58
+ 2. **Ask the human**: "Any corrections or additions?" Wait for their response.
59
+
60
+ 3. **Write payload** to `.gitmem/closing-payload.json`:
61
+ ```json
62
+ {
63
+ "closing_reflection": {
64
+ "what_broke": "...",
65
+ "what_took_longer": "...",
66
+ "do_differently": "...",
67
+ "what_worked": "...",
68
+ "wrong_assumption": "...",
69
+ "scars_applied": ["scar title 1", "scar title 2"],
70
+ "institutional_memory_items": "..."
71
+ },
72
+ "human_corrections": "",
73
+ "scars_to_record": [],
74
+ "open_threads": [],
75
+ "decisions": []
76
+ }
77
+ ```
78
+
79
+ 4. **Call `session_close`** with `session_id` and `close_type: "standard"`
80
+
81
+ For short exploratory sessions (< 30 min, no real work), use `close_type: "quick"` — no questions needed.
82
+ # --- gitmem:end ---