Skip to content

Commit 262593c

Browse files
Claudeclaude
andcommitted
feat: expand starter scar pack to 12 with community-proposed scars (OD-780)
Add 5 community-proposed starter scars covering multi-agent delegation, memory hygiene, and communication patterns. Fix is_active filter, add learning_type to recall results, set explicit is_active on creation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 41b4937 commit 262593c

7 files changed

Lines changed: 270 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [1.3.4] - 2026-02-22
11+
12+
### Added
13+
- **Expanded starter scar pack** (7 → 12): Five new community-proposed scars covering multi-agent delegation, memory hygiene, and communication patterns.
14+
- **Closing payload pre-seeded during init**: `closing-payload.json` template created at install time, preventing Write permission prompt on first session close.
15+
- **`contribute_feedback` tool**: Agents can submit anonymous feedback (feature requests, bugs, friction) to help improve gitmem.
16+
17+
### Fixed
18+
- **`is_active` filter for free tier**: `list()` now treats missing `is_active` as `true` instead of filtering out all learnings without the field.
19+
- **`learning_type` in recall results**: Recall now returns `learning_type` in search results so agents can distinguish scars from wins and patterns.
20+
- **Explicit `is_active: true` on learning creation**: New learnings are created with `is_active: true` to prevent filter mismatches.
21+
1022
## [1.3.1] - 2026-02-22
1123

1224
### Fixed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "gitmem-mcp",
3-
"version": "1.3.3",
3+
"version": "1.3.4",
44
"mcpName": "io.github.gitmem-dev/gitmem",
55
"description": "Persistent learning memory for AI coding agents. Memory that compounds.",
66
"type": "module",

schema/starter-scars.json

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,5 +124,95 @@
124124
"project": "default",
125125
"source_date": "2026-01-01",
126126
"created_at": "2026-01-01T00:00:00Z"
127+
},
128+
{
129+
"id": "5c1111a9-c161-4b2d-83f9-063780164b7c",
130+
"learning_type": "scar",
131+
"title": "Sub-agents Start With Zero Context — Always Inject Memory",
132+
"description": "When you spawn a sub-agent (Task tool), it starts with a blank slate — no conversation history, no institutional memory, no project context. If you don't explicitly inject context via prepare_context or a detailed prompt, the sub-agent will repeat mistakes the team already learned from. The 2-3 seconds to prepare context prevents minutes of wasted work.",
133+
"severity": "high",
134+
"scar_type": "process",
135+
"is_starter": true,
136+
"counter_arguments": [
137+
"The sub-agent has access to the same tools so it can figure things out — but without memory context it will rediscover pitfalls the hard way",
138+
"Injecting context slows down spawning — but 2-3 seconds of prep prevents full task re-runs when the agent hits a known issue"
139+
],
140+
"keywords": ["sub-agent", "task-tool", "context", "delegation", "multi-agent", "prepare-context"],
141+
"domain": ["multi-agent", "delegation"],
142+
"project": "default",
143+
"source_date": "2026-02-22",
144+
"created_at": "2026-02-22T00:00:00Z"
145+
},
146+
{
147+
"id": "f7d1dcb8-dcf1-48fb-9ab0-e682cd704530",
148+
"learning_type": "scar",
149+
"title": "Sub-agents Cannot See Your Conversation — Spell Out Everything",
150+
"description": "Sub-agents spawned via the Task tool do not inherit the parent conversation. They cannot see what the user asked, what files were discussed, or what decisions were made. Every requirement, file path, and constraint must be explicitly stated in the task prompt. Vague delegation like 'fix the bug we discussed' will fail because the sub-agent has no idea what was discussed.",
151+
"severity": "high",
152+
"scar_type": "process",
153+
"is_starter": true,
154+
"counter_arguments": [
155+
"The sub-agent description says it has 'access to current context' — but only certain agent types do, and even then the full nuance of the conversation may not transfer",
156+
"I can just tell it to 'continue the work' — but without explicit requirements the sub-agent will interpret 'the work' differently than you intended"
157+
],
158+
"keywords": ["sub-agent", "delegation", "prompt", "requirements", "context-loss", "multi-agent"],
159+
"domain": ["multi-agent", "delegation"],
160+
"project": "default",
161+
"source_date": "2026-02-22",
162+
"created_at": "2026-02-22T00:00:00Z"
163+
},
164+
{
165+
"id": "060389aa-c0fa-445d-bbfd-5d9250dc1e73",
166+
"learning_type": "scar",
167+
"title": "Don't Pre-read Files Before Delegating — Let the Sub-agent Read Them",
168+
"description": "Reading files into your context before spawning a sub-agent wastes your context window and doesn't help the sub-agent (it can't see what you read). Instead, tell the sub-agent which files to read. The sub-agent has its own context budget and its own file access. Pre-reading just burns tokens twice.",
169+
"severity": "medium",
170+
"scar_type": "process",
171+
"is_starter": true,
172+
"counter_arguments": [
173+
"I need to understand the code before I can write a good prompt — true for architecture decisions, but for 'read and fix' tasks the sub-agent can do its own reading",
174+
"What if the sub-agent reads the wrong files — provide specific file paths in the prompt rather than reading them yourself"
175+
],
176+
"keywords": ["sub-agent", "context-window", "delegation", "file-reading", "tokens", "multi-agent"],
177+
"domain": ["multi-agent", "efficiency"],
178+
"project": "default",
179+
"source_date": "2026-02-22",
180+
"created_at": "2026-02-22T00:00:00Z"
181+
},
182+
{
183+
"id": "55b25d2e-41ad-47f4-8723-569b78586dff",
184+
"learning_type": "scar",
185+
"title": "Search Memory Before Creating Learnings — Duplicates Dilute Recall",
186+
"description": "Before creating a new scar, win, or pattern, search existing memory first. Duplicate entries dilute recall quality — when the same lesson exists three times with slightly different wording, search results waste slots on redundant matches instead of surfacing diverse, relevant knowledge. One well-written scar beats three near-duplicates.",
187+
"severity": "medium",
188+
"scar_type": "process",
189+
"is_starter": true,
190+
"counter_arguments": [
191+
"Creating duplicates reinforces the lesson — but recall is similarity-based, so duplicates just crowd out other useful results",
192+
"My wording is better than the existing one — then update the existing entry instead of creating a new one"
193+
],
194+
"keywords": ["memory", "search", "duplicates", "recall", "learning", "scar-creation"],
195+
"domain": ["memory-hygiene", "gitmem"],
196+
"project": "default",
197+
"source_date": "2026-02-22",
198+
"created_at": "2026-02-22T00:00:00Z"
199+
},
200+
{
201+
"id": "867cf3f6-a0e5-4ca2-8d4d-43cb1f10d94d",
202+
"learning_type": "scar",
203+
"title": "Ask Clarifying Questions Before Coding — 5 Seconds Prevents a Rewrite",
204+
"description": "When requirements are ambiguous, ask a clarifying question before writing code. A 5-second question like 'Should this handle both cases or just the first?' prevents building the wrong thing and having to rewrite. Assumptions that seem obvious are often wrong. The cost of asking is near-zero; the cost of a wrong assumption is a full task redo.",
205+
"severity": "high",
206+
"scar_type": "process",
207+
"is_starter": true,
208+
"counter_arguments": [
209+
"Asking too many questions slows things down — but one targeted question is faster than a rewrite after building the wrong thing",
210+
"I can just build both options and let the user pick — but that's double the work and the user wanted a decision, not a menu"
211+
],
212+
"keywords": ["requirements", "clarification", "assumptions", "communication", "rewrite"],
213+
"domain": ["communication", "planning"],
214+
"project": "default",
215+
"source_date": "2026-02-22",
216+
"created_at": "2026-02-22T00:00:00Z"
127217
}
128218
]

src/services/local-file-storage.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,12 @@ export class LocalFileStorage {
8383
if (options.filters) {
8484
for (const [key, value] of Object.entries(options.filters)) {
8585
const cleanValue = value.startsWith("eq.") ? value.slice(3) : value;
86-
records = records.filter((r) => String(r[key]) === cleanValue);
86+
if (key === "is_active" && cleanValue === "true") {
87+
// is_active defaults to true when not explicitly set
88+
records = records.filter((r) => r[key] !== false);
89+
} else {
90+
records = records.filter((r) => String(r[key]) === cleanValue);
91+
}
8792
}
8893
}
8994

@@ -183,6 +188,7 @@ export class LocalFileStorage {
183188
mapped.push({
184189
id: r.id,
185190
title: String(l.title),
191+
learning_type: String(l.learning_type || "scar"),
186192
description: String(l.description),
187193
severity: String(l.severity || "medium"),
188194
counter_arguments: (l.counter_arguments as string[]) || [],

src/tools/create-learning.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export async function createLearning(
113113
created_at: new Date().toISOString(),
114114
persona_name: agentIdentity,
115115
source_date: new Date().toISOString().split("T")[0],
116+
is_active: true,
116117
// LLM-cooperative enforcement fields (optional)
117118
...(params.why_this_matters && { why_this_matters: params.why_this_matters }),
118119
...(params.action_protocol && { action_protocol: params.action_protocol }),

tests/fixtures/starter-scars.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
/**
22
* Test Fixtures: Starter Scars
33
*
4-
* These match the 3 starter scars that ship with gitmem.
4+
* Representative subset of the 12 starter scars that ship with gitmem.
55
* Used for testing the "fresh install with starter scars" scenario.
66
*/
77

88
import { randomUUID } from "crypto";
99
import type { TestScar } from "./scars.js";
1010

1111
/**
12-
* The 3 starter scars for fresh installations
12+
* Representative subset of starter scars for fresh installations
1313
*/
1414
export const STARTER_SCARS: TestScar[] = [
1515
{

tests/unit/services/local-file-storage.test.ts

Lines changed: 157 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/**
2-
* Unit tests for LocalFileStorage — starter scars timestamp fix
2+
* Unit tests for LocalFileStorage
33
*
4-
* Verifies that loadStarterScars stamps created_at to install time
5-
* instead of using hardcoded dates from starter-scars.json.
4+
* Covers: loadStarterScars, keywordSearch learning_type propagation,
5+
* list() is_active filter handling, and learning_type filtering.
66
*/
77

88
import { describe, it, expect, beforeEach, afterEach } from "vitest";
@@ -82,3 +82,157 @@ describe("LocalFileStorage.loadStarterScars", () => {
8282
expect(learnings).toHaveLength(2);
8383
});
8484
});
85+
86+
describe("LocalFileStorage.keywordSearch — learning_type propagation", () => {
87+
let tmpDir: string;
88+
let storage: LocalFileStorage;
89+
90+
beforeEach(() => {
91+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "gitmem-test-"));
92+
storage = new LocalFileStorage(tmpDir);
93+
94+
// Seed learnings with different types
95+
const learnings = [
96+
{
97+
id: "scar-1",
98+
title: "Database migration rollback",
99+
description: "Always test rollback before migrating",
100+
learning_type: "scar",
101+
severity: "high",
102+
keywords: ["database", "migration"],
103+
is_active: true,
104+
},
105+
{
106+
id: "pattern-1",
107+
title: "Database connection pooling pattern",
108+
description: "Use connection pooling for database access",
109+
learning_type: "pattern",
110+
severity: "low",
111+
keywords: ["database", "connection"],
112+
is_active: true,
113+
},
114+
{
115+
id: "win-1",
116+
title: "Database query optimization win",
117+
description: "Indexing improved database query speed 10x",
118+
learning_type: "win",
119+
severity: "medium",
120+
keywords: ["database", "optimization"],
121+
is_active: true,
122+
},
123+
];
124+
const filePath = path.join(tmpDir, "learnings.json");
125+
fs.writeFileSync(filePath, JSON.stringify(learnings));
126+
});
127+
128+
afterEach(() => {
129+
fs.rmSync(tmpDir, { recursive: true, force: true });
130+
});
131+
132+
it("returns learning_type for each result", async () => {
133+
const results = await storage.keywordSearch("database", 10);
134+
expect(results.length).toBeGreaterThanOrEqual(3);
135+
136+
const types = results.map((r) => r.learning_type);
137+
expect(types).toContain("scar");
138+
expect(types).toContain("pattern");
139+
expect(types).toContain("win");
140+
});
141+
142+
it("does not default all results to scar", async () => {
143+
const results = await storage.keywordSearch("database", 10);
144+
const nonScars = results.filter((r) => r.learning_type !== "scar");
145+
expect(nonScars.length).toBeGreaterThanOrEqual(2);
146+
});
147+
148+
it("preserves learning_type for patterns", async () => {
149+
const results = await storage.keywordSearch("connection pooling", 5);
150+
const pattern = results.find((r) => r.id === "pattern-1");
151+
expect(pattern).toBeDefined();
152+
expect(pattern!.learning_type).toBe("pattern");
153+
});
154+
155+
it("preserves learning_type for wins", async () => {
156+
const results = await storage.keywordSearch("optimization", 5);
157+
const win = results.find((r) => r.id === "win-1");
158+
expect(win).toBeDefined();
159+
expect(win!.learning_type).toBe("win");
160+
});
161+
});
162+
163+
describe("LocalFileStorage.list — is_active filter", () => {
164+
let tmpDir: string;
165+
let storage: LocalFileStorage;
166+
167+
beforeEach(() => {
168+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "gitmem-test-"));
169+
storage = new LocalFileStorage(tmpDir);
170+
171+
// Seed learnings: some with is_active, some without (should default to active)
172+
const learnings = [
173+
{
174+
id: "active-explicit",
175+
title: "Explicitly active",
176+
learning_type: "scar",
177+
severity: "high",
178+
is_active: true,
179+
created_at: "2026-02-22T00:00:00Z",
180+
},
181+
{
182+
id: "active-implicit",
183+
title: "Implicitly active (no is_active field)",
184+
learning_type: "pattern",
185+
severity: "low",
186+
created_at: "2026-02-21T00:00:00Z",
187+
// NOTE: no is_active field — should be treated as active
188+
},
189+
{
190+
id: "archived",
191+
title: "Archived learning",
192+
learning_type: "scar",
193+
severity: "medium",
194+
is_active: false,
195+
created_at: "2026-02-20T00:00:00Z",
196+
},
197+
];
198+
const filePath = path.join(tmpDir, "learnings.json");
199+
fs.writeFileSync(filePath, JSON.stringify(learnings));
200+
});
201+
202+
afterEach(() => {
203+
fs.rmSync(tmpDir, { recursive: true, force: true });
204+
});
205+
206+
it("includes records without is_active when filtering for active", async () => {
207+
const results = await storage.list("learnings", {
208+
filters: { is_active: "eq.true" },
209+
});
210+
const ids = results.map((r) => r.id);
211+
expect(ids).toContain("active-explicit");
212+
expect(ids).toContain("active-implicit");
213+
expect(ids).not.toContain("archived");
214+
});
215+
216+
it("excludes archived records", async () => {
217+
const results = await storage.list("learnings", {
218+
filters: { is_active: "eq.true" },
219+
});
220+
expect(results).toHaveLength(2);
221+
});
222+
223+
it("filters by learning_type correctly", async () => {
224+
const results = await storage.list("learnings", {
225+
filters: { learning_type: "pattern" },
226+
});
227+
expect(results).toHaveLength(1);
228+
expect(results[0].id).toBe("active-implicit");
229+
});
230+
231+
it("combines is_active and learning_type filters", async () => {
232+
const results = await storage.list("learnings", {
233+
filters: { is_active: "eq.true", learning_type: "scar" },
234+
});
235+
expect(results).toHaveLength(1);
236+
expect(results[0].id).toBe("active-explicit");
237+
});
238+
});

0 commit comments

Comments
 (0)