gitmem-mcp 1.2.2 → 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/CHANGELOG.md +16 -0
- package/bin/init-wizard.js +300 -408
- package/bin/uninstall.js +79 -80
- package/dist/services/enforcement.js +12 -14
- package/dist/services/session-state.d.ts +12 -1
- package/dist/services/session-state.js +18 -0
- package/dist/tools/recall.d.ts +1 -0
- package/dist/tools/recall.js +12 -1
- package/package.json +1 -1
- package/schema/starter-scars.json +75 -3
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
196
|
+
log(CHECK, `Removed ${cc.instructionsName}`, "(was gitmem-only)");
|
|
172
197
|
} else {
|
|
173
198
|
writeFileSync(cc.instructionsFile, result.trimEnd() + "\n");
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
359
|
+
log(CHECK, "Removed tool permissions");
|
|
336
360
|
}
|
|
337
361
|
|
|
338
362
|
async function stepGitmemDir() {
|
|
339
363
|
if (!existsSync(gitmemDir)) {
|
|
340
|
-
|
|
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
|
-
|
|
370
|
+
log(CHECK, "Deleted .gitmem/ directory");
|
|
347
371
|
return;
|
|
348
372
|
}
|
|
349
373
|
|
|
350
|
-
|
|
351
|
-
"
|
|
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
|
-
|
|
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(
|
|
380
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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();
|
|
@@ -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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
}
|
|
@@ -18,6 +18,7 @@ interface SessionContext {
|
|
|
18
18
|
agent?: string;
|
|
19
19
|
project?: string;
|
|
20
20
|
startedAt: Date;
|
|
21
|
+
recallCalled: boolean;
|
|
21
22
|
surfacedScars: SurfacedScar[];
|
|
22
23
|
confirmations: ScarConfirmation[];
|
|
23
24
|
reflections: ScarReflection[];
|
|
@@ -29,7 +30,7 @@ interface SessionContext {
|
|
|
29
30
|
* Set the current active session
|
|
30
31
|
* Called by session_start
|
|
31
32
|
*/
|
|
32
|
-
export declare function setCurrentSession(context: Omit<SessionContext, 'surfacedScars' | 'confirmations' | 'reflections' | 'observations' | 'children' | 'threads'> & {
|
|
33
|
+
export declare function setCurrentSession(context: Omit<SessionContext, 'recallCalled' | 'surfacedScars' | 'confirmations' | 'reflections' | 'observations' | 'children' | 'threads'> & {
|
|
33
34
|
surfacedScars?: SurfacedScar[];
|
|
34
35
|
observations?: Observation[];
|
|
35
36
|
children?: SessionChild[];
|
|
@@ -54,6 +55,16 @@ export declare function getProject(): string | null;
|
|
|
54
55
|
* Check if currently working on a Linear issue
|
|
55
56
|
*/
|
|
56
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;
|
|
57
68
|
/**
|
|
58
69
|
* Add surfaced scars to tracking (deduplicates by scar_id)
|
|
59
70
|
* Called by session_start and recall when scars are surfaced.
|
|
@@ -20,6 +20,7 @@ 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: [],
|
|
25
26
|
reflections: [],
|
|
@@ -59,6 +60,23 @@ export function getProject() {
|
|
|
59
60
|
export function hasActiveIssue() {
|
|
60
61
|
return !!(currentSession?.linearIssue);
|
|
61
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
|
+
}
|
|
62
80
|
/**
|
|
63
81
|
* Add surfaced scars to tracking (deduplicates by scar_id)
|
|
64
82
|
* Called by session_start and recall when scars are surfaced.
|
package/dist/tools/recall.d.ts
CHANGED
package/dist/tools/recall.js
CHANGED
|
@@ -19,7 +19,7 @@ import { getProject } from "../services/session-state.js";
|
|
|
19
19
|
import { getStorage } from "../services/storage.js";
|
|
20
20
|
import { Timer, recordMetrics, buildPerformanceData, buildComponentPerformance, calculateContextBytes, } from "../services/metrics.js";
|
|
21
21
|
import { getOrAssignVariant, formatVariantEnforcement, } from "../services/variant-assignment.js";
|
|
22
|
-
import { addSurfacedScars, getCurrentSession } from "../services/session-state.js";
|
|
22
|
+
import { addSurfacedScars, getCurrentSession, setRecallCalled } from "../services/session-state.js";
|
|
23
23
|
import { getAgentIdentity } from "../services/agent-detection.js";
|
|
24
24
|
import { v4 as uuidv4 } from "uuid";
|
|
25
25
|
import * as fs from "fs";
|
|
@@ -36,12 +36,18 @@ function formatResponse(scars, plan, dismissals) {
|
|
|
36
36
|
|
|
37
37
|
No past lessons match this plan closely enough. Scars accumulate as you work — create learnings during session close to build institutional memory.`;
|
|
38
38
|
}
|
|
39
|
+
// First-recall welcome: all results are starter scars
|
|
40
|
+
const allStarter = scars.length > 0 && scars.every((s) => s.is_starter === true);
|
|
39
41
|
// Check if any scars have required_verification (blocking gates)
|
|
40
42
|
const scarsWithVerification = scars.filter((s) => s.required_verification?.blocking);
|
|
41
43
|
const lines = [
|
|
42
44
|
formatNudgeHeader(scars.length),
|
|
43
45
|
"",
|
|
44
46
|
];
|
|
47
|
+
if (allStarter) {
|
|
48
|
+
lines.push(`${dimText("This is your first recall — results will get more relevant as you add your own lessons from real experience.")}`);
|
|
49
|
+
lines.push("");
|
|
50
|
+
}
|
|
45
51
|
// Display blocking verification requirements FIRST and prominently
|
|
46
52
|
if (scarsWithVerification.length > 0) {
|
|
47
53
|
lines.push(`${ANSI.yellow}VERIFICATION REQUIRED${ANSI.reset}`);
|
|
@@ -161,6 +167,9 @@ export async function recall(params) {
|
|
|
161
167
|
const project = params.project || getProject() || "default";
|
|
162
168
|
const matchCount = params.match_count || 3;
|
|
163
169
|
const issueId = params.issue_id; // For variant assignment
|
|
170
|
+
// Mark recall as called BEFORE any search — prevents enforcement false positives
|
|
171
|
+
// when recall returns 0 matching scars
|
|
172
|
+
setRecallCalled();
|
|
164
173
|
// Similarity threshold — suppress weak matches
|
|
165
174
|
// Pro tier: 0.45 calibrated from UX audit (66% N_A rate at 0.35, APPLYING avg 0.55, N_A avg 0.51)
|
|
166
175
|
// Free tier: 0.4 (BM25 scores are relative — top result always 1.0)
|
|
@@ -182,6 +191,7 @@ export async function recall(params) {
|
|
|
182
191
|
counter_arguments: scar.counter_arguments || [],
|
|
183
192
|
applies_when: [],
|
|
184
193
|
similarity: scar.similarity || 0,
|
|
194
|
+
is_starter: scar.is_starter,
|
|
185
195
|
}))
|
|
186
196
|
// Filter below threshold
|
|
187
197
|
.filter((scar) => scar.similarity >= similarityThreshold);
|
|
@@ -342,6 +352,7 @@ export async function recall(params) {
|
|
|
342
352
|
applies_when: scar.applies_when || [],
|
|
343
353
|
source_issue: scar.source_linear_issue,
|
|
344
354
|
similarity: scar.similarity || 0,
|
|
355
|
+
is_starter: scar.is_starter,
|
|
345
356
|
required_verification: scar.required_verification,
|
|
346
357
|
variant_info: variantResults.get(scar.id),
|
|
347
358
|
// LLM-cooperative enforcement fields
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[
|
|
2
2
|
{
|
|
3
|
-
"id": "
|
|
3
|
+
"id": "81fe2d44-24ae-4946-a063-d4ea3a12a71a",
|
|
4
4
|
"learning_type": "scar",
|
|
5
5
|
"title": "Done != Deployed != Verified Working",
|
|
6
6
|
"description": "Code being 'done' (merged) doesn't mean it's deployed, and being deployed doesn't mean it's working correctly in production. The full loop is: merge → deploy → verify the feature works end-to-end in the target environment. Skipping verification leads to silent failures.",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"created_at": "2026-01-01T00:00:00Z"
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
|
-
"id": "
|
|
21
|
+
"id": "07c7f9d3-1336-4fef-81da-af7c97590ab9",
|
|
22
22
|
"learning_type": "scar",
|
|
23
23
|
"title": "Database Migration Without Rollback Plan",
|
|
24
24
|
"description": "Running database migrations without a tested rollback plan risks data loss or extended downtime. Destructive migrations (dropping columns, changing types) are especially dangerous. Always write and test the down migration before running the up migration in production.",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"created_at": "2026-01-01T00:00:00Z"
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
|
-
"id": "
|
|
39
|
+
"id": "33996174-a7f4-42f5-81ac-f601e566cc5a",
|
|
40
40
|
"learning_type": "scar",
|
|
41
41
|
"title": "Silent Error Swallowing Hides Real Failures",
|
|
42
42
|
"description": "Empty catch blocks and generic error handlers that log but don't surface errors lead to silent failures. The system appears to work while data is lost or corrupted. At minimum, log errors with enough context to diagnose the issue. Better: fail visibly so problems are caught early.",
|
|
@@ -52,5 +52,77 @@
|
|
|
52
52
|
"project": "default",
|
|
53
53
|
"source_date": "2026-01-01",
|
|
54
54
|
"created_at": "2026-01-01T00:00:00Z"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"id": "726c03d4-8962-4b0f-89a2-ea1cd0b223aa",
|
|
58
|
+
"learning_type": "scar",
|
|
59
|
+
"title": "Untested Code Passes By Coincidence",
|
|
60
|
+
"description": "Code without tests may appear to work because the happy path succeeds, but untested edge cases silently fail. Tests that don't exist can't catch regressions. 'It works on my machine' is not a test — write assertions for the behavior you expect, especially boundary conditions and error paths.",
|
|
61
|
+
"severity": "high",
|
|
62
|
+
"scar_type": "process",
|
|
63
|
+
"is_starter": true,
|
|
64
|
+
"counter_arguments": [
|
|
65
|
+
"The code is simple enough that tests aren't needed — but simple code gets modified later and regressions appear without test coverage",
|
|
66
|
+
"Manual testing is sufficient for this — but manual tests don't run automatically and get skipped under time pressure"
|
|
67
|
+
],
|
|
68
|
+
"keywords": ["testing", "coverage", "regression", "assertions", "edge-cases"],
|
|
69
|
+
"domain": ["testing", "quality"],
|
|
70
|
+
"project": "default",
|
|
71
|
+
"source_date": "2026-01-01",
|
|
72
|
+
"created_at": "2026-01-01T00:00:00Z"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": "8a376e29-dc8d-49a7-b840-28643086e90a",
|
|
76
|
+
"learning_type": "scar",
|
|
77
|
+
"title": "Environment Config Drift Between Local and Production",
|
|
78
|
+
"description": "Environment variables, feature flags, and configuration that differ between local dev and production cause bugs that only appear after deployment. The fix works locally but breaks in prod because of a missing env var, different API URL, or stricter security policy. Keep a canonical list of all config, validate it at startup, and test with production-like config.",
|
|
79
|
+
"severity": "high",
|
|
80
|
+
"scar_type": "operational",
|
|
81
|
+
"is_starter": true,
|
|
82
|
+
"counter_arguments": [
|
|
83
|
+
"We use the same .env file everywhere — but .env files drift silently and new variables get added locally without updating production",
|
|
84
|
+
"Our staging environment matches production — but staging configs still diverge over time without automated drift detection"
|
|
85
|
+
],
|
|
86
|
+
"keywords": ["environment", "config", "env-vars", "production", "deployment", "drift"],
|
|
87
|
+
"domain": ["config", "deployment"],
|
|
88
|
+
"project": "default",
|
|
89
|
+
"source_date": "2026-01-01",
|
|
90
|
+
"created_at": "2026-01-01T00:00:00Z"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"id": "b6a9ac29-25ba-4667-9fed-0ada98dd588e",
|
|
94
|
+
"learning_type": "scar",
|
|
95
|
+
"title": "Changing Dependencies Without Checking Breaking Changes",
|
|
96
|
+
"description": "Upgrading or adding dependencies without reading the changelog introduces breaking changes that surface later as mysterious failures. Semver violations are common — a minor version bump can still break your code. Always read the changelog, check for breaking changes, and run your test suite after any dependency change.",
|
|
97
|
+
"severity": "medium",
|
|
98
|
+
"scar_type": "process",
|
|
99
|
+
"is_starter": true,
|
|
100
|
+
"counter_arguments": [
|
|
101
|
+
"It's just a patch version so it should be safe — but semver is a social contract, not a guarantee, and patch releases do contain breaking changes",
|
|
102
|
+
"Our lockfile protects us — but lockfiles only help if everyone uses them consistently and CI doesn't do fresh installs"
|
|
103
|
+
],
|
|
104
|
+
"keywords": ["dependencies", "npm", "upgrade", "breaking-changes", "changelog", "semver"],
|
|
105
|
+
"domain": ["dependencies", "maintenance"],
|
|
106
|
+
"project": "default",
|
|
107
|
+
"source_date": "2026-01-01",
|
|
108
|
+
"created_at": "2026-01-01T00:00:00Z"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"id": "5db0ffc7-e6d8-48bc-aa23-fce7fd8eaadb",
|
|
112
|
+
"learning_type": "scar",
|
|
113
|
+
"title": "Fixing Symptoms Instead of Root Causes",
|
|
114
|
+
"description": "Adding workarounds, special cases, or retry logic to mask a bug treats the symptom while the root cause remains. The workaround becomes tech debt that makes the real fix harder to find later. Before writing a fix, trace the execution path to understand WHY the failure happens, not just WHERE it happens.",
|
|
115
|
+
"severity": "high",
|
|
116
|
+
"scar_type": "process",
|
|
117
|
+
"is_starter": true,
|
|
118
|
+
"counter_arguments": [
|
|
119
|
+
"We need a quick fix now and can investigate later — but 'later' rarely comes and the workaround becomes permanent",
|
|
120
|
+
"The root cause is in a system we don't control — but understanding the root cause still helps write a better workaround with clear documentation"
|
|
121
|
+
],
|
|
122
|
+
"keywords": ["debugging", "root-cause", "workaround", "tech-debt", "symptoms"],
|
|
123
|
+
"domain": ["debugging", "engineering"],
|
|
124
|
+
"project": "default",
|
|
125
|
+
"source_date": "2026-01-01",
|
|
126
|
+
"created_at": "2026-01-01T00:00:00Z"
|
|
55
127
|
}
|
|
56
128
|
]
|