You may not record a lesson without two arguments against it

GitMem

Scars are refused at write time unless the author supplies at least two counter-arguments, and a PreToolUse hook hard-blocks consequential actions until every surfaced scar is applied, ruled inapplicable, or refuted with a risk acknowledgment.

Carries 1 of 7 rubric mechanisms. Most systems here carry none or one (41%), and a dash means the mechanism was not found at this commit — not that the system needed it. Each mark is one LLM reviewer's reading of the code at this commit rather than a run of it — known limits.

  • Tombstone
  • Trust state
  • Bi-temporal
  • Scope enforced
  • Mutation audit
  • Human review
  • Negative evals

1. Executive Summary

GitMem is an MIT MCP server — around 59,000 lines of TypeScript — that gives a coding agent "institutional memory": scars (mistakes), wins, patterns and anti-patterns, in Postgres with pgvector or a local .gitmem/ directory.

Two mechanisms make it worth the report, and they are the same idea applied at both ends of the memory lifecycle.

At write time, a scar is refused unless it argues against itself.

function validateScar(params: CreateLearningParams): string[] {
  const errors: string[] = [];
  if (!params.severity) {
    errors.push("Scars require severity (critical, high, medium, low)");
  }
  if (!params.counter_arguments || params.counter_arguments.length < 2) {
    errors.push("Scars require at least 2 counter_arguments");
  }
  return errors;
}

You may not record a lesson learned without recording at least two reasons someone might reasonably reject it. The counter-arguments are stored in a TEXT[] column, folded into the embedding text, and returned on every search — so the objection travels with the claim to the point of use.

Nothing else in this atlas requires a memory to carry its own rebuttal.

At read time, every surfaced scar must be individually answered. confirm_scars implements what the file calls the refute-or-obey protocol:

"Each recalled scar must be addressed with: APPLYING — Scar is relevant, past-tense evidence with artifact reference; N_A — Scar doesn't apply, scenario comparison required; REFUTED — Overriding scar, risk acknowledgment required."

And it is enforced, not requested. hooks/scripts/recall-check.sh is a PreToolUse hook on Bash with a "CONFIRMATION GATE (hard block, consequential actions only)" that emits {"decision": "block"} when recall-source scars are unconfirmed. A sibling hook, credential-guard.sh, hard-blocks any tool call that would expose credentials.

Daem0nMCP makes consultation a precondition. GitMem makes consultation per item: it is not enough to have called recall, you must say what you did about each thing recall told you, and overriding one costs you an explicit risk acknowledgment.

And the one clean bug is in the dismissal counter — section 9.

2. Mental Model

A scar is a structured artifact, not a sentence: severity, problem_context, solution_approach, applies_when, why_this_matters, action_protocol, self_check_criteria, and the counter-arguments. Recall surfaces it, the agent must answer it, and the answer is recorded along with whether the resulting action succeeded.

Diagram — a scar is refused without a severity and two counter-arguments, a PreToolUse hook blocks a consequential command while surfaced scars are unconfirmed, and a repeat mistake is recorded against the scar that failed to prevent it
Diagram source
%% caption: a scar is refused without a severity and two counter-arguments, a PreToolUse hook blocks a consequential command while surfaced scars are unconfirmed, and a repeat mistake is recorded against the scar that failed to prevent it
flowchart TD
    CL["create_learning(type='scar')"] --> V{"validateScar"}
    V -->|"no severity, or fewer than 2 counter_arguments"| REJ["write refused, errors returned"]
    V -->|ok| INS["gitmem_learnings row<br/>counter_arguments folded into the embedding text"]
    Q["recall / search(project)"] --> S["surfaced scars, counter_arguments included"]
    S --> CF["confirm_scars"]
    CF --> A1["APPLYING — past-tense evidence + artifact reference"]
    CF --> A2["N_A — scenario comparison required"]
    CF --> A3["REFUTED — risk acknowledgment required"]
    A1 --> ST["session state"]
    A2 --> ST
    A3 --> ST
    BASH["agent attempts a consequential Bash call"] --> HK{"PreToolUse: recall-check.sh"}
    HK -->|"recall-source scars unconfirmed"| BLK["{'decision':'block'}"]
    HK -->|confirmed| GO["allowed"]
    GO --> U["gitmem_scar_usage:<br/>reference_type explicit/implicit/acknowledged/refuted/none,<br/>surfaced_at, acknowledged_at, referenced, execution_successful"]
    RM["the same mistake happens again"] --> RMF["repeat_mistake = true,<br/>related_scar_id, repeat_mistake_details"]
    RMF --> AN["analytics: report the scars that failed to prevent recurrence"]

3. Architecture

src/ splits into tools (the MCP surface), schemas (Zod input schemas per tool), services, hooks, commands, diagnostics and constants. The storage layer is Supabase/Postgres with pgvector on the paid path and a local .gitmem/ directory on the free tier, and search.ts implements both: a vector path and a local keyword path that also scans decisions and merges them into the same ranked list.

hooks/ ships a Claude Code plugin with SessionStart, UserPromptSubmit, PreToolUse and session-close hooks, plus templates for Claude, Cursor, Copilot and Windsurf rule files.

Five separate vitest configs — unit, integration, e2e, perf and smoke — and 82 test files.

4. Essential Implementation Paths

Refuse a weak scarsrc/tools/create-learning.ts (validateScar :39-51, buildEmbeddingText :54-69, the early return on errors :80-90).

Confirm a surfaced scarsrc/tools/confirm-scars.ts (the protocol docstring :1-18), src/services/session-state.ts.

Block on unconfirmedhooks/scripts/recall-check.sh (the confirmation gate :6-9), hooks/scripts/credential-guard.sh, hooks/hooks/hooks.json (the PreToolUse matcher on Bash).

Record what happenedsrc/tools/record-scar-usage.ts, record-scar-usage-batch.ts, schema/setup.sql gitmem_scar_usage :132-147.

Suggest and dismisssrc/services/thread-suggestions.ts (SUGGESTION_MATCH_THRESHOLD :29, the match loop :91-107, the new suggestion :120-127, dismissSuggestionById :157-168, getPendingSuggestions :174-180), src/tools/dismiss-suggestion.ts.

5. Memory Data Model

gitmem_learnings is the table to read:

  • learning_typeCHECK IN ('scar', 'win', 'pattern', 'anti_pattern')
  • severityCHECK IN ('critical', 'high', 'medium', 'low')
  • counter_arguments TEXT[] — the arguments against this memory
  • applies_when TEXT[], problem_context, solution_approach
  • why_this_matters, action_protocol, self_check_criteria
  • is_active, decay_multiplier
  • repeat_mistake BOOLEAN, related_scar_id UUID, repeat_mistake_details JSONB
  • project, source_date, persona_name, embedding vector(1536)

repeat_mistake is the field to steal. A scar exists to stop something happening again. This schema records the case where it didn't — the flag, a link to the original scar, and a JSONB detail with a reason — and services/analytics.ts queries repeat_mistake: "eq.true", joins related_scar_id and repeat_mistake_details, and reports them.

A memory system that measures which of its memories failed at their job is rare. The atlas has read many that measure retrieval and almost none that ask whether retrieval changed the outcome.

gitmem_scar_usage is the other half:

reference_type TEXT CHECK (reference_type IN
  ('explicit', 'implicit', 'acknowledged', 'refuted', 'none')),
surfaced_at TIMESTAMPTZ, acknowledged_at TIMESTAMPTZ,
referenced BOOLEAN, execution_successful BOOLEAN, variant_id UUID

'none' is the value most systems omit: a scar was shown and the agent did nothing with it. surfaced_at and acknowledged_at separate shown from addressed. execution_successful closes the loop to the outcome, and variant_id allows A/B variants of the same lesson.

6. Retrieval Mechanics

Vector search on the Supabase path, keyword on the free tier, with severity and learning_type post-filters and a fetchCount = matchCount * 3 over-fetch so the post-filter has material to trim. Decisions are searched alongside learnings on the local path and merged into one similarity-sorted list.

The project key is resolved, passed, and dropped. Every caller computes it the same way — params.project || getProject() || "default" — and hands it to localScarSearch(query, fetchCount, project). That third parameter is declared _project, and the function body is two lines: fetch the singleton and call instance.search(query, k). The key never reaches the search. The file says so where the singleton is defined: "Unified cache — all scars loaded into single instance regardless of project. At ~400 scars, semantic similarity handles relevance better than project partitioning. Project params kept in signatures for backward compat but ignored for cache lookup." getLocalVectorSearch, initializeLocalSearch, reinitializeLocalSearch, isLocalSearchReady and getCacheMetadata all take the same underscore-prefixed, ignored argument, and both call sites in startup.ts pass undefined anyway, over a load the comment labels "Load ALL scars from Supabase (cross-project unified cache)".

The remote fallback is explicit about it rather than accidental. project_filter is deliberately not sent, and the comment gives the reasoning: the primary path this stands in for is the unified cross-project cache, so filtering here "would make the fallback return a different, narrower result set than the path it stands in for — a silent behaviour change on exactly the cold-start calls that are hardest to notice." That is a defensible choice given a cross-project primary, and it is the second place the key is dropped on purpose.

So retrieval is cross-project by design, at about four hundred scars, on the argument that semantic similarity beats partitioning at that size. It is a real position and it is not scope enforcement: no read path applies a stored project key, and scope_enforced is not earned.

counter_arguments come back on every result shape in search.ts. That is the detail that makes the write-time rule worth having: the objection is not filed away, it is injected next to the claim.

7. Write Mechanics

create_learning validates, embeds and inserts. A scar failing validation returns success: false with the errors and no row — a refusal, not a warning.

archive_learning sets is_active = false, and log.ts filters is_active: "eq.true", so archiving removes a learning from retrieval without deleting it. Nothing is keyed on a rejected value: an archived scar's content, re-submitted, is a new row.

8. Agent Integration

An MCP server on npm (gitmem-mcp) with a one-command init wizard that auto-detects the IDE, a Claude Code plugin with four lifecycle hook points, rule templates for four editors, a PRIVACY.md and a SECURITY.md, and a starter scar set in schema/starter-scars.json.

The hook set is the integration that matters: SessionStart initialises, UserPromptSubmit auto-retrieves, PreToolUse guards credentials and enforces confirmation, and a session-close check runs at the end.

9. Reliability, Safety, and Trust

Scope enforced — withheld, per section 6. The key is stored on the row and resolved on every call, and the search functions take it as _project and ignore it in favour of one cross-project cache. A parameter that reaches a signature and stops there is the read-path form of a declared-and-unwired mechanism, and the project states the design in the source rather than leaving it to be inferred.

Human review — awarded on the suggestion pipeline: implicit thread detection proposes a thread from session-embedding similarity, the suggestion is status: "pending", and it becomes an open thread only when a person promotes it or disappears when a person dismisses it. A proposal that does not take effect until someone acts on it is the mark's substance.

Audit log — withheld, deliberately. gitmem_scar_usage is the richest retrieval-feedback table in this corpus, and retrieval feedback is explicitly the half the mark does not cover: it records what was surfaced and used, not an append-only record of memory mutations. Nothing found logs creates, archives and edits as events.

Trust state — withheld. is_active is a lifecycle flag and severity and decay_multiplier are grades, not epistemic states. The nearest thing to an epistemic state is repeat_mistake, which describes the world rather than the memory's standing.

Tombstone, bitemporal, negative eval — no. source_date is stored and no query treats it as a validity bound.

A retrieval fallback answered PGRST202 on every call for as long as it existed, and the repository is the source for that. The Supabase scar-search fallback built its RPC name from the table prefix with a verb appended — gitmem_scar_search by default, orchestra_scar_search under a prefix — and PostgREST exposes neither; the deployed functions are match_<table> and match_<table>_weighted. The comment left behind states the consequence and why it went unnoticed: the call "returned PGRST202 on every call, on every deployment, since it was written — invisible because it is only reached while the local vector index is cold." A path that runs only during warm-up, and returns an error the caller treats as no results, is indistinguishable from a genuine miss; the pair of names differ in more than spelling, since the weighted variant is the one returning the decay_multiplier recall consumes and it takes match_threshold where its sibling takes similarity_threshold. A committed test pins the names.

The confirmation gate could be satisfied by a retrieval that failed. confirm_scars is the enforcement point for the refute-or-obey protocol, and a fix titled "confirm_scars must not green-light a failed retrieval" separates the two cases with a test beside it. A review gate whose precondition is a successful recall has to be able to tell a failed recall from an empty one, and that distinction is the same one the RPC bug made invisible.

The dismissal counter cannot reach its own threshold.

dismiss_suggestion's header says: "Suggestions dismissed 3+ times are permanently suppressed." getPendingSuggestions implements it: s.status === "pending" && s.dismissed_count < 3.

But dismissSuggestionById sets suggestion.status = "dismissed" and increments the count, and the routine that decides whether an incoming topic matches an existing suggestion begins:

for (const suggestion of updated) {
  if (suggestion.status !== "pending") continue;

A dismissed suggestion is skipped by the matcher. So the next time the same topic recurs, no existing suggestion matches, and a fresh one is created with generateSuggestionId() and dismissed_count: 0. In the paths read, nothing returns a dismissed suggestion to pending.

Therefore dismissed_count can never exceed 1, the < 3 guard is unreachable, and dismissal suppresses a record rather than a topic — the user who dismisses a suggestion will be offered the same topic again under a new id. The unit test asserts exactly the reachable behaviour (expect(result! .dismissed_count).toBe(1)), which is why the gap is invisible to the suite.

The fix is small: match against dismissed suggestions too (they carry the embedding), and increment the existing record instead of creating a new one. That would also turn the feature into something this atlas has been looking for — a rejected item keyed on its content rather than its id, which is the property that separates a real suppression from a bypassable one.

10. Tests, Evals, and Benchmarks

No paper, no retrieval benchmark. 92 test files across five vitest configs — unit, integration, e2e, perf and smoke — plus CI, a CHANGELOG.md, a SECURITY.md, a PRIVACY.md, a CODE_OF_CONDUCT.md and a DIRECTORY-SUBMISSIONS.md.

Notable test names: no-console-log.test.ts (a lint invariant asserted as a test), provenance-citation.test.ts, confirm-scars.test.ts, archive-learning.test.ts, thread-suggestions.test.ts.

The measurement that would matter here is the one the schema is built for and no committed analysis performs: gitmem_scar_usage.execution_successful joined against reference_type, answering whether confirming a scar changed the outcome. The repeat_mistake analytics report is the closest, and it counts recurrences rather than comparing them against a baseline.

I ran nothing.

11. For Your Own Build

Steal

  • Require counter-arguments before you accept a lesson. Two, minimum, enforced at write time with the write refused otherwise. It forces the author to have thought about when the lesson does not apply, and it is the cheapest guard against a memory system that accumulates confident overgeneralisations.
  • Return the counter-arguments with the memory. Storing the objection and hiding it at retrieval would be pointless; here it rides along in every result shape and in the embedding text.
  • Make consultation per item, not per session. Every surfaced scar answered with APPLYING, N_A or REFUTED — and make each answer cost something: past-tense evidence with an artifact reference, a scenario comparison, or an explicit risk acknowledgment.
  • Enforce it with a hard block on consequential actions only. {"decision": "block"} from a PreToolUse hook matched to Bash, with read-only tools unaffected, is the right blast radius.
  • Record 'none' as a usage outcome. A scar that was surfaced and ignored is data; a table that only records uses cannot tell you which memories are dead weight.
  • Separate surfaced_at from acknowledged_at. Two timestamps make "shown" and "addressed" distinguishable, which is what makes the ignore rate measurable.
  • Track whether the scar failed. repeat_mistake, related_scar_id and a reason, reported in analytics. A memory system should be able to name its own memories that did not work.
  • Give a learning an action_protocol and self_check_criteria. A lesson an agent can act on beats a lesson it can only read.
  • Over-fetch before post-filtering. matchCount * 3 when a severity or type filter is set.

Avoid

  • Do not skip non-pending records in a dedup matcher when the non-pending state is what you are trying to enforce. Here it makes a documented suppression rule unreachable and turns "dismissed permanently" into "dismissed once, then offered again".
  • Do not key a suppression on a generated id. Key it on the content or its embedding, or a re-derivation walks straight past it.
  • Do not let a test assert only the reachable branch. The suite checks dismissed_count becomes 1 and never asks how it would become 3.

Fit

The best fit in this batch for a team that wants an agent to be accountable to its memory rather than merely informed by it — the refute-or-obey gate plus the counter-argument requirement is a coherent discipline, and the hooks make it real rather than advisory.

The Supabase dependency is the main adoption question; the free tier's local keyword path exists but is a different retrieval quality.

12. Open Questions

  • Who sets repeat_mistake? The analytics read it and the write path was not located; whether it is agent-reported or detected was not established.
  • What is variant_id for? gitmem_scar_usage supports A/B variants of a scar and no variant assignment logic was found.
  • Is execution_successful ever analysed? It is the column that would answer whether confirmation changes outcomes.
  • Does decay_multiplier reach ranking? It is on the row; its consumer was not traced.

Appendix: File Index

Write-time validationsrc/tools/create-learning.ts (validateScar :39-51, embedding text including counter-arguments :54-69, the refusal path :80-90)

Confirmation protocolsrc/tools/confirm-scars.ts (the refute-or-obey docstring :1-18), src/schemas/confirm-scars via src/schemas/index.ts, src/services/session-state.ts

Hookshooks/hooks/hooks.json (SessionStart, UserPromptSubmit, PreToolUse on Bash), hooks/scripts/recall-check.sh (the confirmation gate :6-9, the output contract :23), hooks/scripts/credential-guard.sh (the "decision": "block" emissions), hooks/scripts/session-close-check.sh

Schemaschema/setup.sql (gitmem_learnings :11-39, gitmem_sessions :68-85, gitmem_decisions :104-118, gitmem_scar_usage :132-150), schema/starter-scars.json

Retrievalsrc/tools/search.ts (project resolution :131, over-fetch :136, the local path :140-200, counter-arguments in every result shape :151, :269, :301)

Suggestionssrc/services/thread-suggestions.ts (SUGGESTION_MATCH_THRESHOLD :29, the thread cover check :80-88, the pending-only match loop :91-107, the new suggestion :120-127, dismissSuggestionById :157-168, getPendingSuggestions :174-180), src/tools/dismiss-suggestion.ts (the suppression claim :4-5), tests/unit/services/thread-suggestions.test.ts (:271-278)

Analyticssrc/services/analytics.ts (repeat_mistake: "eq.true" :229, the select :236, the report :357-361, :808)

History

2026-09-10d47a625f… — read again, 19 commits past the previous pin, and scope_enforced is withdrawn as a first-reading error. The mark was awarded on project being resolved per call and passed into localScarSearch(query, fetchCount, project); that third parameter is _project and the body calls instance.search(query, k), so the key stops at the signature. The singleton it fetches is one cross-project cache — "all scars loaded into single instance regardless of project… Project params kept in signatures for backward compat but ignored for cache lookup" — filled by two startup.ts calls that pass undefined over a load labelled "Load ALL scars from Supabase (cross-project unified cache)", and the remote fallback omits project_filter deliberately so it matches the path it stands in for. None of this moved between the pins: src/services/local-vector-search.ts is unchanged since the previous reading and its last commits date to February 2026, one of them titled "fix: session refresh project context, cross-project recall, and thread cascade". The claim was wrong when written, in the direction of crediting a mechanism the code documents itself as not having. human_review holds and carries an evidence record. Two defects in the mechanism paths were fixed upstream and are recorded here as the project's own history: the scar-search fallback had built its RPC name from the table prefix with a verb appended and "returned PGRST202 on every call, on every deployment, since it was written", and confirm_scars could green-light a failed retrieval. The published dismissed_count defect was re-run and stands — dismissal sets the status as well as the counter, so the < 3 guard is still unreachable. Test files went from 82 to 92. Screened before reading: two auto-run surfaces, no manifest inside the seven-day cooldown, one build-time execution path and two unpinned dependency surfaces; nothing was installed, built or run.

2026-08-09c091a758… — first reading. Screened before reading; the tree was read, never installed, and no test was run.