Purpose: define the two-layer memory system that captures recurring lessons, user preferences, and project context across sessions and contributors. The first layer is a single project-instruction file read by every contributor; the second is a memory directory of focused, loadable-on-demand entries.
This system exists because the same mistake otherwise gets made repeatedly across sessions and contributors. Write it down once, in the right place, and the next person — human or AI — does not relearn the lesson at cost.
A project that lives longer than a few weeks accumulates lessons faster than any single contributor can remember:
These are all knowledge transfer problems. Code review and documentation help partially; chat history helps not at all (it is ephemeral and unsearchable in practice). Without a deliberate system, every new contributor — whether they’re human-new or just a fresh AI session — pays full re-learning cost.
The memory system solves this by deliberately writing down the lessons in a format that future contributors can find and use.
A single file at the repo root (e.g., CLAUDE.md, CONTRIBUTING.md, AGENTS.md — pick a name and stay consistent). This is the one file every contributor reads on their first day and re-reads on every session.
It must be:
A directory of focused memory entries, typically at memory/ (or .<agent>/memory/ for per-agent memory). Each entry is a small markdown file covering one lesson.
It must be:
MEMORY.md index file at the root lists every entry as a one-line link.The instruction file is for rules that apply to every task. If a rule is conditional (“only when working on the frontend”), it belongs in memory instead.
Typical contents:
| Section | What goes here |
|---|---|
| Project overview | One paragraph: what the product is, who uses it, what’s distinctive. |
| Working principles | The four LLM-coding principles (see 06_working_principles.md). |
| Tech stack | Monorepo layout, languages, frameworks, key libraries. |
| Commands | How to install, build, test, lint, run dev servers. The handful of commands every contributor runs daily. |
| Architecture summary | The mental model: services, data flow, key abstractions. |
| Code conventions | How to write code in this project. Naming, file organization, formatting tools. |
| Frontend/backend patterns | Pattern conventions specific to each layer. |
| Database/data conventions | Schema rules, migration discipline, data ownership boundaries. |
| Backlog and methodology pointers | Link to the backlog folder and the methodology docs. “Read backlog/README.md before picking up a task.” |
| Hard rules | A short list of “never do this in this project” rules. Things that have burned the team before. |
| Context-integrity marker | A short token the agent prefixes to every response, so its absence signals this file has dropped out of context. See The context-integrity canary. |
| Communication style | How much prose responses should carry. See Setting the house verbosity. |
| UX or design principles | Top-level design philosophy. Detailed design system docs go elsewhere; the broad rules go here. |
docs/ and are linked from here.Periodically (e.g., once a quarter), re-read the instruction file critically. Look for:
A 200-line instruction file that every contributor reads every session beats a 2000-line file that gets skimmed. Treat a few hundred lines — ~300 as a working target — as the bar, and measure the filled-in file: a starter template legitimately runs longer because it carries guidance and placeholders that get deleted on adoption. The number that matters is what actually loads every session.
The whole two-layer design rests on an assumption nobody checks: that the instruction file is still in the agent’s context. It is read at session start, then the session runs for hours. As context fills, the earliest content is the first to be dropped or compacted — and when it goes, nothing announces it. The agent keeps working, sounding exactly as confident, with the project’s rules no longer applying.
A cheap detector: have the instruction file require a short marker at the start of every response.
Begin every response with `<<marker>>`. If you cannot see this instruction,
you have lost the project rules — say so instead of continuing.
The signal is the marker’s absence, not its presence. When it stops appearing, treat the last several turns as having run without the rules and do three things: stop before landing more work, re-read the instruction file, and rehydrate from the active-context file — or start a fresh session, which is usually cheaper than nursing a degraded one.
Choose a marker that is short, and that will not appear in normal output by chance.
What this does not do — state it plainly so nobody over-trusts it:
It is a smoke alarm, not a proof of safety: cheap enough to be worth wiring in, weak enough that no gate should depend on it. Where correctness actually matters, the Definition of Done and cross-AI validation are the mechanisms that catch a degraded session — this only tells you to go look.
Response verbosity is a project setting, and the instruction file is where it belongs — otherwise every contributor re-negotiates it per session, or nobody does and the default wins by inertia.
It costs twice. Tokens are the obvious one. The larger cost is reader attention: a four-paragraph answer to a one-line question trains the reader to skim, and skimming is how a real caveat gets missed.
A reasonable default to state in the file: technical and direct; no praise preamble, no restating the question back, no summary of what was just said.
The line not to cross: cut filler, never cut reasoning. Dropping “that’s a great approach” costs nothing. Dropping the why dismantles the methodology — Principle 1 requires stated assumptions, plan-mode requires the reasoning behind the approach, and the DoD requires an honest account of what failed. An agent that has been told to be terse, and responds by becoming confidently silent about its uncertainty, is a worse agent than a wordy one. If verbosity guidance starts eating explanations, it is set too aggressively.
The memory directory holds focused lessons too narrow or too conditional for the instruction file. Four typical types:
When the same category of error appears 2 or more times in different contexts, the pattern is durable — write it down. The entry captures:
These are usually named feedback_<topic>.md to mark their nature.
How the user (or operator, or the team) wants contributors to behave. Examples:
These are usually named feedback_<behavior>.md (the line between behavioral feedback and recurring-fix feedback is fuzzy; both get feedback_ prefix).
Non-obvious facts about the project that aren’t derivable from reading the code or git history. Examples:
These are usually named project_<topic>.md or by domain.
Pointers to external systems where information lives. Examples:
These are usually named reference_<topic>.md.
Every memory entry is a markdown file with frontmatter and a body. The frontmatter is structured; the body has a small required shape.
---
name: <short-kebab-case-slug>
description: <one-line summary for the index>
metadata:
type: feedback | project | reference | user
---
<Body — one or two paragraphs stating the rule or fact.>
**Why:** <The reason for the rule. Usually a past incident or strong
preference. This is what lets future-you judge edge cases instead of
blindly applying the rule.>
**How to apply:** <When and where this kicks in. The trigger conditions
or scope.>
name — kebab-case slug, also used as the filename (<name>.md). Examples: feedback-no-emojis, project-beta-wave, reference-grafana-dashboards.description — a single line that appears in the index. Should be specific enough that a contributor can decide from the index whether the entry is relevant to their current task.metadata.type — one of feedback, project, reference, or user. Determines how the entry is meant to be applied.metadata.status (optional) — active (the default; omit it), stale (flagged for archival on the next sweep), or archived (kept for lineage, dropped from the live index). See Archive, don’t destroy.metadata.pinned (optional) — true marks a load-bearing entry (one that guards against data loss, a security regression, or an irreversible action) that sweeps never auto-archive, however rarely it is referenced.Two mandatory components:
Why: — the rationale. Without the rationale, the rule is brittle; future contributors cannot tell when an edge case warrants deviation.And one strongly recommended:
How to apply: — the trigger conditions. Where and when does this rule kick in?Symptoms: — for recurring-fix entries, how to recognize the problem.Anti-pattern: — what to avoid.Related: — cross-links to other memory entries (use [[other-name]] syntax for memory-internal links).Most entries should be 30–100 lines of markdown. Long memory entries are often signs that the content belongs in a regular doc (like a pillar or a design document) instead.
The index file at the root of the memory directory (MEMORY.md) lists every entry as a single line:
# Memory Index
- [Title](file.md) — one-line hook describing when this matters.
- [Another title](other-file.md) — one-line hook.
- ...
A concrete example (note the kebab-slug filename + actionable hook):
# Memory Index
- [definition-of-done.md](definition-of-done.md) — Every task tested on actual UI in fix-test loop before commit; CHANGELOG + README updated after each meaningful change.
- [git-branch-protection.md](git-branch-protection.md) — Feature branches for new work; merge to master only via PRs; never force-push.
- [feedback-no-permission-prompts.md](feedback-no-permission-prompts.md) — Skip "may I…?" — just make the change unless it's destructive.
feedback, project, etc.) or archive entries that are no longer load-bearing.A new contributor (or an AI agent in a fresh session) sees the index but cannot read every entry. The hook is how they decide which entries to load. A vague hook (“about CSS”) forces loading the entry to know if it applies; a specific hook (“never use the non-existent –color-stone-50 var; use –card-bg”) tells them at a glance whether their current task is touched by it.
There are four reliable triggers. If none of these apply, you probably do not need a new memory.
The user says “remember this” or “save this as a memory” or words to that effect. Write the entry immediately. Use the type they implied or, if unclear, pick the best fit (usually feedback or project).
If you’ve had to correct the same kind of error in two different sessions or two different contexts, that’s a pattern. Write a feedback entry capturing the fix.
(The “2+ occurrences ⇒ promote” bar is the same one used elsewhere in the methodology — e.g., backlog items that recur land as patterns rather than one-offs. It’s the threshold that separates “noise” from “rule.”)
The bar for this is category of error, not literal identical error. If the same kind of CSS variable mistake has happened twice (different specific variables, but same pattern), it counts.
A decision that future-you would not be able to reconstruct from reading the code. Common signals:
A project entry captures these.
The user confirms an unusual approach worked (“yes, that’s the right call; keep doing that”). This is the positive counterpart to a correction — the user has just told you a non-default choice is correct for this project. Capture it as a feedback entry.
Without this trigger, the memory drifts toward “list of corrections” and loses the validated choices the team has accepted. Both kinds of feedback are worth saving.
Saving things that don’t belong as memory is worse than not saving them — the memory directory bloats, the index becomes unscannable, and the actual durable lessons get lost in the noise.
git log / git blame. Don’t snapshot it into memory.Before saving, ask: will this still be useful and correct in six months?
The first “do not save” rule above generalizes into the sharpest single filter for both layers. If a contributor can learn it by reading the repo right now, it is read live — never written down. What earns a line is only what the source cannot tell you:
| Not stored (derivable) | Stored (not derivable) |
|---|---|
The test command — it’s in package.json |
“The suite takes eleven minutes, so run the focused file first” |
| The directory layout — it’s on disk | “legacy/ is mid-migration; new code goes in core/” |
| Which library handles dates — it’s in the imports | “We moved off X because of its DST bug; don’t reintroduce it” |
| That a function exists — grep finds it | “That function looks generic but is load-bearing for billing” |
Four things pass this test: intent, rationale, policy, and observed pitfalls. Everything else is a snapshot of a moving target, and a snapshot is worse than nothing — it goes stale silently, and a confidently wrong instruction file costs more than an absent one.
This is also why a consolidation pass ends smaller or equal, never larger. An audit that grows the file has usually re-derived things the code already says. The size budgets give the same rule a number; this gives it a criterion.
The asymmetry worth knowing: admission is strict, but retirement is not symmetric with it. A policy or pitfall retires when the thing it guards is gone — not when it stops being mentioned. A rule that is working erases its own evidence: nobody reintroduces the DST-buggy library, so the entry looks inert precisely because it succeeded. Low reference frequency is never grounds for removal; see Archive, don’t destroy and the pinned flag.
Memory (above) is durable — lessons and facts meant to outlive many sessions. But a contributor mid-task also carries volatile state: what they are doing right now, what just changed, what comes next. What NOT to save rules this out of memory deliberately — yet it still needs a home, because an AI session loses it on every context reset, and a fresh session (or a different agent picking up the work) starts blind.
Keep this state in a single active-context file — one short, fast-changing markdown file, separate from durable memory. A common location is backlog/ACTIVE_CONTEXT.md (project-wide), or one per active epic; for AI-agent work it can live alongside the agent’s own files. Whatever the location, there is one per work-stream and it is expected to churn.
BL-###) or goal being worked right now.Keep it to a screen. It is a baton, not a journal: when an item closes, its lines are cleared or overwritten, not accumulated. The permanent record lives in commits, the item’s ARCHIVE.md entry, and — for durable lessons — memory.
The file earns its keep at the two moments a session’s working memory is most fragile:
git log, the item’s Status: / Lock:) per Memory and current state — the file is a claim about where things were, not proof of where they are now.Durable memory answers “what have we learned?”; active context answers “where am I?”. Mixing them rots both: volatile state stuffed into memory makes the index churn and buries durable lessons, while a durable lesson left in a scratch file gets cleared on the next reset. Keep the durable in memory/, the volatile in the active-context file. The one bridge between them — if something written as volatile context keeps reappearing (the same “next step” recurs across items), that is a Trigger 2 signal to promote it into a real memory entry.
Memory decays. Projects evolve. A memory entry written six months ago may now be wrong (the rule changed, the workaround is no longer needed, the file it references no longer exists). Without maintenance, the memory directory becomes a graveyard of stale advice.
When a memory entry names a specific function, file, or flag, that’s a claim it existed when the memory was written. It may have been renamed or removed since.
Before recommending or relying on a memory entry that names specifics:
“The memory says X exists” is not the same as “X exists now.” Trust but verify.
Memory captures a snapshot at the time it was written. For current state — what is in the repo now, what was recently changed, what the active set of items is — prefer reading the live state (git log, the actual files, the live backlog) over recalling a memory.
When a recalled memory conflicts with current state, trust what is observable now, and update or delete the stale memory rather than acting on it.
Every few months (or whenever the memory directory feels noisy), do a consolidation pass:
The consolidation pass is itself a kind of work. It can be filed as a backlog item with effort estimate S (half a day for most projects).
A memory entry moves through states: active → stale → archived. True deletion is reserved for entries that never carried durable value. The reflex to delete a stale entry throws away its lineage — the Why:, the incident that produced it, the record that the team once held this belief. When an entry was a real lesson but no longer applies, archive it instead of deleting it.
metadata.status: stale with the date. If it is still unreferenced at the next sweep, archive it. This one-sweep delay protects entries that are merely dormant — tied to a paused subsystem or a seasonal workflow — from being removed the moment they go quiet.memory/archive/ (or set metadata.status: archived and drop its index line). It leaves the live index — so it no longer costs context on every session — but the lesson and its Why: stay discoverable to anyone who greps. Archived entries are out of the way, not gone.metadata.pinned: true. A sweep never marks a pinned entry stale or archives it, no matter how rarely it is referenced — low reference frequency on a “never drop the prod database” rule means the rule is working, not that it is dead.Git history is the ultimate backup: a deleted entry is always recoverable via git log / git show. The archive is not about recoverability — it is about discoverability. A grep of memory/archive/ surfaces “we used to believe X, and here is why we stopped” without git archaeology, and that lineage is often the fastest way to avoid re-introducing a problem the team already solved.
If a memory’s name turns out to be a poor description, rename the file and update the index and update any [[other-name]] cross-references in other memory entries. Treat renames as small atomic changes.
---
name: feedback-trust-internal-types
description: Don't add defensive type-checks for inputs the type system has already validated.
metadata:
type: feedback
---
Internal-to-internal function calls do not need defensive type-checks
at the receiving side. If the type system says the input is `User`,
treat it as `User`. Re-validating with `if (!user || typeof user.id
!== 'string')` clutters the code and obscures the actual logic, and
the check cannot fail because the type system has already proved it.
This applies only to *internal* boundaries (your own code calling
your own code, with shared types). At *external* boundaries (user
input, network responses, filesystem reads), validation is required
because the type system cannot enforce contracts across the boundary.
**Why:** This came up after a code review where a contributor added
`assertIsUser(user)` calls inside every function that took a `User`
parameter. The result: 200 lines of dead code, no observable benefit,
slower reads. The cleanup took a day. The pattern persisted because
nobody had named the rule.
**How to apply:** When writing or reviewing internal functions: if
the parameter type already encodes the constraint you're about to
check, delete the check. Reserve defensive validation for trust
boundaries (HTTP route handlers, file parsers, third-party API
responses, user input from forms).
**Anti-pattern:** Wrapping internal calls in try/catch "just in case."
If the call cannot throw a recoverable error, the try/catch hides
bugs without protecting against anything real.
**Related:** [[feedback-simplicity-first]]
The entry is short, specific, has a Why:, has a How to apply:, and uses one cross-reference to a sibling memory. A new contributor reading it understands the rule, the rationale, and the boundary case in a minute.
---
name: project-content-deletion-flow
description: Content deletions must happen on dev first; prod sync re-creates rows otherwise.
metadata:
type: project
---
Content tables (places, articles, narratives, tags) flow one direction
only: dev → prod via the scheduled content sync. There is no prod →
dev path for content. If you delete content directly on prod, the
next content sync re-creates it from the dev dump.
Authoritative deletion workflow:
1. Delete on dev (via admin UI or the merge-duplicates script).
2. Verify dev is clean.
3. Run the content sync to push dev's state to prod. The sync removes
the prod row in passing.
**Why:** A team member deleted a duplicate place on prod, the next
sync re-created it, and they thought they'd fixed it. Confusion cost
two days. The directionality of the sync was not documented anywhere
visible, so the lesson was easy to repeat.
**How to apply:** Any time you're about to delete content data on
prod, stop and route the change to dev first. The only safe direct-
on-prod delete is one immediately mirrored on dev before the next
sync.
**Related:** Content sync deploy command lives in `scripts/deploy.bat
content`; behavior documented in the project instruction file.
This entry could not be derived from reading the code. The Why: makes the danger concrete. The How to apply: gives the rule.
When you find yourself filing memory entries that cluster around the same theme — three different feedback_* entries about deploy verification, five about a class of CSS-variable mistakes, four about platform-specific quirks — that cluster is a signal: the methodology itself probably has a gap that’s producing the recurring lessons.
Single memory entry → an individual lesson
Two related memory entries → a recurring lesson; might just be a coincidence
Three or more related → the methodology is missing something
When you see three, do the work:
Memory entries are symptoms. The methodology is the system. If the symptoms are recurring, the system has a defect — patching individual symptoms forever is not the right response.
When the methodology absorbs the lesson, the memory entries become redundant. You can prune them, or leave them as historical context with a note pointing at the methodology.
Quarterly:
This closes the loop: real practice → memory → methodology updates → fewer recurring lessons. Without this loop, memory becomes a graveyard and the methodology becomes stale.
Sometimes the methodology gets ahead of practice — a section is written that the team hasn’t yet needed. That’s fine; the methodology is aspirational on those edges. But if a year passes and the section was never needed, consider whether it should be pruned or marked as optional.
The healthy state: methodology and memory inform each other continuously. Both stay current.
The memory system, the project-instruction file, and the methodology docs form a layered hierarchy of durability. Mistakes flow upward when they prove they need to:
One-off correction → handled in conversation; nothing written.
Same correction 2+ times → write a memory entry. (See "When to write" above.)
Memory entry referenced often → consider promoting to the instruction file.
Cluster of 3+ related entries → consider absorbing into the methodology.
Each layer raises the bar for what survives. Memory is project-specific. The instruction file is universal-within-the-project. The methodology is universal-across-projects. Promoting a rule moves it from one scope to a broader one, and demands a higher level of confidence that the rule will hold.
Covered above under When to write a new memory, Trigger 2. Once the same category of mistake has occurred twice in different contexts, the cost of leaving it unwritten exceeds the cost of writing it. Capture the pattern.
A memory entry has graduated and belongs in the instruction file when:
When promoting:
This is the upward limit. A rule belongs in the methodology when:
userId parameter in route X” is not.Methodology additions go via the patterns in Memory as a leading indicator for methodology gaps.
A rule that holds in the calm of the moment it was written can still fail under the conditions where rules actually get broken. Before promoting one up a layer (memory → instruction file, or instruction file → methodology), stress it against the situations that break rules:
Why: strong enough to justify the loss.A rule that survives the stress earns promotion — and should carry the reasoning that got it there. A rule that cracks gets narrowed, kept advisory in memory, or dropped. Promoting a brittle rule just teaches contributors to route around the layer it lives in.
Without the named loop: mistakes get fixed in conversation, forgotten, recur, get fixed again, recur, get added to memory once someone notices, sit in memory forever even after they’ve graduated, and never reach the instruction file or the methodology where they would prevent the failure mode systematically.
Naming the loop names the trigger. The trigger triggers the promotion. The promotion makes the rule durable.
Rules also flow downward when they no longer hold:
The healthy state: each rule lives at the lowest layer that captures its actual scope. No higher, no lower.
| Mistake | Fix |
|---|---|
Memory entry has no Why:. |
Add the rationale. Without it, edge cases cannot be judged. |
| Memory entry duplicates a rule already in the instruction file. | Delete the memory; the instruction file is the universal rule. |
| Memory entry is a wish (“we should use library X”). | File as a backlog item instead. Memory is for facts and rules, not aspirations. |
| Memory entry has gone stale (file/function it references no longer exists). | Update it, or archive it if the lesson once mattered. Stale memory left in the live index is worse than no memory. |
Stale entry was hard-deleted, losing its Why: and lineage. |
Archive instead (memory/archive/ or metadata.status: archived). Reserve outright deletion for entries that never carried durable value. |
| Index has an entry the body doesn’t, or vice versa. | Audit and reconcile. The index is the navigational truth. |
| Memory directory is unmaintained for over a year. | Run a consolidation pass. Prune, merge, sharpen. |
| Same lesson is captured in two entries with different angles. | Merge into a single entry; reference both angles in the body. |
| Memory entry is hundreds of lines long. | Probably belongs as a regular doc (pillar, architecture refinement, design doc). Memory is for short, focused lessons. |
| User explicitly said “forget this” but the entry is still there. | Delete the entry. Honor explicit forget requests. |
| Memory references session-specific context (“the bug I fixed last Tuesday”). | Generalize the lesson. Session context decays; the durable pattern does not. |
| Volatile “where am I” state (current focus, next steps) stuffed into a memory entry. | Move it to the active-context file. Memory is for durable lessons; the active-context file is for session state. |
Memory entries are advisory. They are accumulated experience, not authoritative rules. They are intended to make contributors faster and to prevent repeated mistakes, not to override explicit current direction.
When a memory entry conflicts with current user direction, current direction wins. The memory may need updating (and probably does — the user explicitly disagreed with the captured rule). Surface the conflict, ask whether to update or delete, and proceed.
When two memory entries appear to conflict, the contributor should surface the conflict to the user. Memory entries are not refereed by an editor; conflicts are resolved by humans.
The project-instruction file is more authoritative than memory. Memory cannot override the instruction file; only the user or an explicit project decision can update the instruction file. When in doubt, the instruction file wins.
Working principles (06_working_principles.md) and Definition of Done (07_definition_of_done.md) are foundational; memory cannot override either. Memory adds project-specific color on top of these universal layers.
Next: 09 — Git workflow