Skip to content

Jonwhitefang.uk

Stardate 2026.238 · systems nominal
All prompts
Agent documentUpdated 2026-07-11

Portable Memory Spine

Give a coding agent project memory that survives sessions, machines, and tools.

Source: JonWhiteFang/Documents · guides/portable-memory-spine.md

Raw markdown

What you're reading. A self-contained, copy-paste-ready guide to a small system that gives an AI coding agent durable, version-controlled memory of a project — memory that survives across sessions, machines, and across different agents/tools, because it lives in the repository itself.

This guide is project-agnostic. The pattern and all templates below work for any software project. A real implementation (a Unity factory-automation game called Yuletide Works) is used throughout as a worked example, always clearly boxed and labelled EXAMPLE — copy the generic templates, not the example text.

Hand this whole file to an agent in another project and say "set this up for us." (§4.6 is the definition of done for that request.)


1. The core idea

AI coding agents are stateless between sessions. Most agent tools offer some "memory," but it is almost always machine-local and tool-specific: it lives in a hidden directory on one developer's laptop, isn't shared with teammates, doesn't travel with the repo, and is invisible to a different agent or a human reading the project later.

This system takes the opposite stance:

Project memory is part of the project. It is committed to the repository, version-controlled, reviewed in pull requests, and readable by any agent or human who clones the repo.

It does this with a handful of plain Markdown files plus two pieces of automation. Together they form a read-at-start / write-at-end loop:

                    ┌───────────────────────────────────────────────┐
                    │  Repo-committed memory spine (plain Markdown) │
                    │   • PROJECT_STATE.md   (live one-page truth)  │
                    │   • RUN_LOG.md         (prepend-only history) │
                    │   • DECISIONS/*.md     (ADRs — the "why")     │
                    └───────────┬───────────────────────▲───────────┘
                                │ reads                 │ writes
                                ▼                       │
        START of session                     END of session
   ┌────────────────────────────┐          ┌──────────────────────┐
   │ session-start hook         │          │  /checkpoint routine │
   │ injects git state + top of │          │  doc-drift sweep →   │
   │ STATE into agent context   │          │  ADR (if needed) →   │
   └────────────────────────────┘          │  STATE → RUN_LOG     │
                                           └──────────────────────┘

The contract that keeps it healthy: the spine tracks progress and decisions only. It points at the authoritative docs (architecture, coding rules, scope) — it never restates them. That single discipline is what stops the spine from going stale and contradicting itself. (More on this in §4.)

Why each piece exists

PieceAnswers the questionLifecycle
PROJECT_STATE.md"Where are we right now?"Living — overwritten every session; kept to ~one page.
RUN_LOG.md"What happened, and when?"Prepend-only — new entries go on top; past entries never rewritten.
DECISIONS/ (ADRs)"Why did we choose this?"Frozen — written once; superseded by new ADRs, not edited.
session-start hookBootstraps the agent's context automatically.Automation (tool-specific).
/checkpoint routinePersists memory consistently at session end.Automation (a skill / prompt / habit).

The three time-horizons — now (STATE), history (RUN_LOG), rationale (ADRs) — are deliberately separated so each file has exactly one job and one update rule. Don't merge them.


2. The five components (with copy-paste templates)

Create a docs/ directory and a docs/DECISIONS/ subdirectory. (Another docs home works too, but every path below — including the hook in §2.4 and the checkpoint routine in §2.5 — assumes docs/; if you relocate the spine, update them all together.) The five components below go in the places shown. Everything is plain Markdown except the hook (a shell script) and the settings file (JSON).

Throughout, <…> marks a placeholder to replace. Keep the structure; swap the content.


2.1 docs/PROJECT_STATE.md — the one-page live snapshot

Job: the single "current truth" of where the build is. Read it first when resuming work. Keep it to roughly one screen — detail goes to RUN_LOG.md, rationale goes to DECISIONS/. Overwrite it each session; it is not a log.

It has a stable skeleton: a header note, Current position, a phase/milestone tracker, an optional scope-gate tracker, Next actions, Open questions, an optional scope ledger, and References. Drop the trackers you don't need, but keep "Current position / Next actions / Open questions" — those are the minimum that makes a resume cheap.

# Project State — <Project Name>

> **One-page living snapshot.** This is the committed, version-controlled "current truth" of
> where the build is. Keep it to roughly one screen — push detail into [RUN_LOG.md](RUN_LOG.md)
> and [DECISIONS/](DECISIONS/). Update it at the end of every working session.
>
> This file does **not** restate architecture, invariants, or scope rules — those live in
> <the project guide / README> and the planning docs. This file tracks *progress and live decisions only*.

_Last updated: <YYYY-MM-DD> (<one-line "what changed + what's next", so the head tells the story>)_

---

## Current position

- **Stage:** <current phase/milestone + a sentence on what concretely exists vs. doesn't>
- **Active gate:** <which review/quality gate is in play, or "none reached">
- **Branch focus:** <which branch is current; where the next work should start>
- **Verification:** <last known-good state: build status, tests passing, manual confirm — with numbers>

---

## Phase / milestone tracker

Status legend: `[ ]` not started · `[~]` in progress · `[x]` done & verified · `[!]` blocked.
Source of truth for each item's scope and acceptance criteria: <link to roadmap/plan doc>.
That doc owns *scope*; this table is the authority for current *status* — don't mirror status back into the plan.

| # | Title | Status | Notes |
|--:|-------|:------:|-------|
| 0 | <foundation / scaffold>                | `[ ]` | <gate or constraint> |
| 1 | <first vertical capability>            | `[ ]` | <gate or constraint> |
| 2 | <next>                                 | `[ ]` | <…> |
| … | …                                      | `[ ]` | … |

---

## Scope-gate tracker  *(optional — keep if the project has staged scope)*

| Gate | Definition | Status |
|------|------------|:------:|
| **<Prototype>**     | <smallest end-to-end proof; the bar it must clear> | `[ ]` not started |
| **<MVP>**           | <next scope ceiling> | `[ ]` not started |
| **<V1>**            | <full scope> | `[ ]` not started |

---

## Next actions (explicit order)

1. <the very next concrete thing to do — specific enough to start cold>
2. <…>

---

## Open questions / decisions pending

These are flagged-but-unresolved. When decided, mark RESOLVED — via an ADR (with link) if the decision
is non-trivial and lasting (see DECISIONS/), inline with a one-line outcome otherwise.

- **<question>** — <why it's open / what's blocked on it>. → ADR (if non-trivial) when decided.
- **<question, once decided>** → **RESOLVED** ([ADR-NNNN](DECISIONS/ADR-NNNN-<slug>.md)): <one-line outcome>.

---

## Scope ledger  *(optional — the documented defense against scope creep)*

Anything deliberately deferred out of the current stage gets logged here so it is a **decision, not a leak**.

| Item | Decision | Revisit at |
|------|----------|------------|
| <tempting out-of-scope feature> | Deferred | <which gate/phase> |

> When a task tempts you toward one of these before its gate, add a row here with the temptation and the
> deferral rationale **rather than building it.**

---

## References

- Decisions: [`DECISIONS/`](DECISIONS/) — <list the key ADRs with one-line summaries>.
- Run history: [`RUN_LOG.md`](RUN_LOG.md).
- Architecture, invariants, scope, build order: <link to the authoritative project guide>.
- Planning docs: <links>.

EXAMPLE (Yuletide Works). The phase tracker has 16 rows (Phase 0 → MVP review gate); two are [x] done with verification baked into the Notes cell (EditMode 55/55, PlayMode 5/5, user visual confirm passed). The scope-gate tracker has four gates (Prototype → MVP → Vertical slice → Version 1). The scope ledger has a standing table of 13 features explicitly deferred to "V1" (magical power, fluids, recycling, multi-floor, …) plus per-phase deferral tables added as each phase makes a deferral decision. The _Last updated:_ line is a paragraph that summarises the latest merge and names what's next — so the auto-injected head (see §2.4) tells the whole story without opening the file.


2.2 docs/RUN_LOG.md — prepend-only session history

Job: the narrative of what happened, when. One entry per working session, newest on top. Keep entries short — anything still current belongs in PROJECT_STATE.md; rationale belongs in an ADR. This file is prepend-only: new entries go at the top; past entries are never rewritten (they are point-in-time records). Two branches that both add an entry will conflict at the top of the file — the resolution is trivial: keep both entries, newest date first. When the file grows unwieldy (say, a year of entries), roll older entries into docs/RUN_LOG-YYYY.md and link the archive at the bottom; the active file keeps recent history.

# Run Log — <Project Name>

> Prepend-only history of working sessions. New entries at the **top**; never edit past entries. One entry per session.
> Keep entries short — detail that's still *current* belongs in [PROJECT_STATE.md](PROJECT_STATE.md);
> decisions belong in [DECISIONS/](DECISIONS/). This log is the *narrative of what happened, when*.

---

## Entry template (copy this)

```
## YYYY-MM-DD — <short title>
- **Goal:** <what this session set out to do>
- **Did:** <what actually changed — files, decisions, builds>
- **Verified:** <commands/tests run + their results, or "n/a — no code yet">
- **Open / blockers:** <what's unresolved>
- **Memory updated:** STATE ☐ · RUN_LOG ☑ · ADR ☐ (<id if any>)
```

---

## <YYYY-MM-DD> — <most recent session title>

- **Goal:** <…>
- **Did:** <…>
- **Verified:** <…>
- **Open / blockers:** <…>
- **Memory updated:** STATE ☑ · RUN_LOG ☑ · ADR ☐

EXAMPLE (Yuletide Works). The very first entry recorded the spine being scaffolded (Verified: n/a — no game code yet). Later entries cite real verification — Verified: compile 0 errors, EditMode 55/55, PlayMode 5/5 — and the Memory updated: checkboxes make it auditable which artifacts each session touched.


2.3 docs/DECISIONS/ — Architecture Decision Records (the "why")

Job: capture why each non-trivial call was made — the context, the options weighed, the choice, the consequences — so a future contributor (or future you, or a future agent) doesn't have to reverse-engineer it. An ADR is written once and then largely frozen; later thinking lands in a new ADR that supersedes the old one, never as an edit to the original.

This subdirectory holds three kinds of file: a template, an index (README.md), and the numbered ADRs themselves.

When to add one (important): add an ADR only when a real decision with alternatives and lasting consequences was made — a structural call, a tooling commitment, a guardrail, a deliberate deviation from the docs. Routine work — implementing a planned feature, fixing a bug, writing tests — does not earn an ADR. (The /checkpoint routine in §2.5 enforces this.)

2.3.1 docs/DECISIONS/ADR-TEMPLATE.md

The metadata lives in YAML front matter (the MADR 4.x convention) so both humans and agents can parse it without reading the body. supersedes / superseded-by make the relations machine-readable too.

---
status: Proposed | Accepted | Enacted | Superseded by ADR-XXXX | Deprecated
date: YYYY-MM-DD
deciders: <who>
phase: <e.g. Phase 0, MVP>
supersedes: <ADR id(s), or omit>
superseded-by: <ADR id, added once superseded>
---

# ADR-NNNN: <decision title>

## Context

What problem are we solving? What forces are at play (technical constraints, the governing
architectural rules, scope stage, the project invariants)? Why does this need a decision now?

## Decision

What we decided, stated plainly. One paragraph.

## Alternatives considered

- **A — <option>:** why considered / why not chosen.
- **B — <option>:** …
- **C — <option>:** …

## Consequences

- **Positive:** what this makes easier or safer.
- **Negative / trade-offs:** what this costs or constrains.
- **Follow-ups:** new tasks, risks to watch, things to revisit at a later gate.

## Confirmation

How will we know the code still follows this decision? Name a concrete check — a test, an assertion,
a lint/CI rule, a review-checklist item, or "manual review of <X>". This is what lets a later
doc-drift sweep verify the decision is still *enacted*, not just recorded.

## Links

- Related ADRs: <ids>
- Planning docs: <which docs this draws on>
- Commits / PRs: <if any>

2.3.2 The status lifecycle (matters more than it looks)

StatusMeaning
ProposedWritten and under consideration; not yet committed to.
AcceptedThe decision is agreed and binding, but not necessarily reflected in code yet.
EnactedAccepted and realised in the codebase — the code matches the decision today.
Superseded by ADR-XXXXReplaced by a later decision; kept for history. The named ADR is now authoritative.
DeprecatedNo longer applies and not directly replaced.

The Accepted → Enacted distinction is the useful part: it lets you record a decision before you've built it, then flip the status when the code lands — without rewriting the ADR. A single ADR can also be partly superseded (e.g. its tooling-version section is replaced by a later ADR while the rest stands); note that in the index rather than editing the body.

The immutability rule, precisely: an ADR's decision content (Context / Decision / Alternatives considered / Consequences) is written once and never edited. The mutable parts are the pointers around it: the YAML front matter (status, superseded-by), the Confirmation section (it names the current verification check, so update it when the check moves — a renamed test, a CI rule replacing a manual review), the Links section (append later commits/PRs/related ADRs; don't remove entries), and the ADR's row in the index. Add a dated annotation in the index when a change needs explaining.

2.3.3 docs/DECISIONS/README.md — the index

A one-row-per-decision roll-up so the ADRs are navigable. Keep it in sync as you add ADRs.

# Architecture Decision Records — Index

> **Purpose:** the single entry point for *why* the non-trivial calls were made — one row per decision,
> with status, scope, and how the ADRs relate.

This index complements <the authoritative project guide>, which holds the **WHAT** (architecture,
invariants, scope) and the **WHY at the level of standing principle**. The ADRs are the **HOW /
point-in-time detail**. When the project guide and an ADR appear to disagree, the guide is the *current
rule* and the ADR is the *historical record* — check the **Status** column first.

## Status legend

| Status | Meaning |
|---|---|
| **Proposed** | Written; not yet committed to. |
| **Accepted** | Agreed and binding, but not necessarily in code yet. |
| **Enacted** | Accepted *and* realised in the codebase today. |
| **Superseded by ADR-XXXX** | Replaced by a later decision; kept for history. |
| **Deprecated** | No longer applies, not directly replaced. |

## The index

| ID | Title | Status | Date | Phase | Relations | Summary |
|---|---|---|---|---|---|---|
| [ADR-0001](ADR-0001-<slug>.md) | <title> | <status> | <date> | <phase> | <none / supersedes / pairs-with> | <one-line summary> |

## How to add a new ADR

1. **Pick the next number** (never reused, even if an ADR is later superseded).
2. **Choose a short slug:** `ADR-NNNN-<short-slug>.md`, kept in this directory.
3. **Copy the template;** fill in the front matter and the body. If it **supersedes** an existing ADR,
   set `supersedes:` in the new ADR's front matter and `superseded-by:` (plus `status`) in the old
   one's. If it merely **pairs with** another ADR, link them under **Links → Related ADRs** only — the
   supersession keys are for lifecycle replacement, nothing else. (The index's *Relations* column
   summarises both kinds of link.)
4. **Wire it into the index** (a row above) and link it from `PROJECT_STATE.md` → References.

2.3.4 docs/DECISIONS/ADR-0001-<slug>.md — your first ADR

A strong first ADR is the architecture baseline: it records the governing principle and invariants the whole project rests on, as a decision (with the alternatives that were rejected), so later ADRs can reference or supersede specific parts. Use the template above. Even though the baseline may feel "obvious," writing it down captures the why and the rejected alternatives before any code exists.

Do not invent one. Derive ADR-0001 from the project's existing docs, code, or explicit answers from your human. If no architecture baseline actually exists yet, leave it as an open question in PROJECT_STATE.md rather than fabricating a decision to fill the slot.

EXAMPLE (Yuletide Works). ADR-0001 records the "simulation is the truth; Unity is the representation" four-layer split and its invariants, listing the rejected alternatives (Unity-object- authoritative state; a class-per-building-type hierarchy; deferring save/load). It is Accepted, but its engine-version section was later superseded by ADR-0005 — captured as a "Lifecycle note" in the index, not by editing ADR-0001. The repo now has six ADRs spanning architecture, test framework, save format, build verification, engine version, and a per-phase design ADR.


2.4 The session-start hook — auto-inject memory at the start of every session

Job: so the agent doesn't have to be told to read the spine, a start-of-session hook injects the current git state + the top of PROJECT_STATE.md straight into the agent's context. The agent resumes already knowing where things stand.

This is the one tool-specific component. The script below is written for Claude Code's SessionStart hook, which expects a JSON object on stdout with a hookSpecificOutput.additionalContext string. See §4.3 for how to adapt it to other agents/tools — the idea (inject git state + STATE head at session start) is portable even where the exact mechanism isn't.

.claude/hooks/session-preflight.sh (make it executable: chmod +x):

#!/usr/bin/env bash
# SessionStart "Context Preflight" hook.
# Injects current git state + the top of docs/PROJECT_STATE.md into the agent's context
# at the start of each session, so work resumes against the committed memory spine.
# Output goes to the agent via the SessionStart hook's additionalContext field.
set -euo pipefail

# Hooks run with $CLAUDE_PROJECT_DIR set to the project root; fall back to cwd.
DIR="${CLAUDE_PROJECT_DIR:-$PWD}"

ctx="$(
  {
    echo '## Git state'
    git -C "$DIR" status --short --branch 2>/dev/null || echo '(not a git repo)'
    echo
    echo '## Recent commits'
    git -C "$DIR" log --oneline -10 2>/dev/null || true
    echo
    echo '## docs/PROJECT_STATE.md (top — read this, then the latest docs/RUN_LOG.md entry)'
    head -n 60 "$DIR/docs/PROJECT_STATE.md" 2>/dev/null || echo '(PROJECT_STATE.md not found)'
  }
)"

jq -n --arg ctx "$ctx" \
  '{hookSpecificOutput: {hookEventName: "SessionStart", additionalContext: $ctx}}'

.claude/settings.json (wires the hook in; this file is committed so the whole team gets it):

{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/session-preflight.sh\"",
            "timeout": 15,
            "statusMessage": "Loading project memory spine…"
          }
        ]
      }
    ]
  }
}

Note: the hook depends on jq (for safe JSON encoding), git, and a Unix shell — on Windows, run it via Git Bash/WSL or port it to PowerShell; without jq, build the JSON with python3 -c (or any tool that escapes strings correctly). The injected content is what matters, not the mechanism. The head -n 60 is why PROJECT_STATE.md's top — especially the _Last updated:_ line — is written to summarise the whole story: those first lines are what the agent sees automatically before reading anything else.


2.5 The /checkpoint routine — persist memory consistently at session end

Job: before wrapping up a session, run a short, repeatable routine that (a) does a fast doc-drift sweep to keep the docs honest, then (b) adds an ADR only if a real decision was made, (c) updates PROJECT_STATE.md, and (d) prepends a RUN_LOG.md entry that links the rest. This is what makes the spine reliable instead of "updated when someone remembers."

In Claude Code this is packaged as a skill (a callable /checkpoint command). In any other tool it works equally well as a saved prompt, a runbook, or simply a habit the agent is told to follow at end of session. Either way, the steps and guardrails below are the real content.

.claude/skills/checkpoint/SKILL.md (Claude Code skill form — the body is portable prose either way):

---
name: checkpoint
description: Use at the end of a working session, before finishing or committing, to persist project
  memory and keep the docs current — runs a fast doc-drift sweep (fixing stale status/version/link
  drift), adds an ADR if a non-trivial decision was made, updates docs/PROJECT_STATE.md, and prepends
  a docs/RUN_LOG.md entry.
---

# Checkpoint — persist project memory

Run this before wrapping up a session so the **committed memory spine** reflects reality. The spine
lives in the repo (version-controlled, team-shareable) — distinct from any tool's machine-local
auto-memory.

## Steps

1. **Review what changed this session.** Run `git status` and `git diff --stat`. Recall the session's
   goal, what actually changed, and what's still open.

2. **Doc-drift sweep** (keep the docs honest — a *fast, mechanical* pass, not a full audit). Establish
   **ground truth from code/config/git, not from another doc** (a stale doc citing a stale doc proves
   nothing), then scan for drift against it:
   - **Status drift.** Anything naming a phase/stage, "complete/planned/pending", PR numbers, or dates
     as a *current* claim — does it still hold? (`git log`, the tracker.) Usual offenders: `README.md`
     and any "Last updated" / "Status:" lines.
   - **Version / fact drift.** Tool versions, dependency versions, module/file names, IDs, paths,
     command targets — grep the value and confirm it matches the project's actual config files.
   - **Broken internal links.** Verify the target of each changed/added relative `.md` link exists.
   - **Duplication that drifted.** If two docs state the same fact and now disagree, the one that isn't
     the authority is the bug — fix it to *point at* the authority rather than re-state it.

   **Scope it to the session.** Default to the docs this session plausibly affected (what `git diff`
   touched, plus `README` / the project guide / the spine, which drift most). A full repo-wide audit is
   a separate, on-demand task — don't run it here.

   **Fix policy:**
   - **Auto-fix** unambiguous drift (stale status line, wrong version string, dead link, a duplicated
     fact that should be a pointer). Make the minimal edit.
   - **Never rewrite history.** `RUN_LOG.md` entries and ADR decisions are point-in-time records — do
     not "correct" them. If an ADR's body states a fact later superseded, record the supersession in its
     front matter (`status`, `superseded-by`) and in the index (with a dated note); never edit the body
     itself.
   - **Surface, don't guess.** If a discrepancy needs a judgment call, list it in the report for the
     user instead of editing.

   Record what you fixed (and what you flagged) in the RUN_LOG entry (Step 5).

3. **Add an ADR** *only if* a non-trivial decision was made this session (a choice with alternatives and
   consequences). Copy `docs/DECISIONS/ADR-TEMPLATE.md` to `docs/DECISIONS/ADR-NNNN-<slug>.md` (next
   number in sequence), fill it in, and add its row to the `DECISIONS/README.md` index. Steps 4 and 5
   link to it.

4. **Update `docs/PROJECT_STATE.md`** (keep it ~one page):
   - Bump `_Last updated:_` to today (with a one-line "what changed + what's next").
   - Move any item `[ ]`→`[~]`→`[x]` / `[!]` in the tracker.
   - Update the scope-gate tracker if a gate was reached.
   - Refresh **Next actions** and **Open questions** (linking any ADR from Step 3).
   - If you were tempted toward an out-of-scope item and deferred it, add a row to the **scope ledger**.

5. **Prepend an entry to `docs/RUN_LOG.md`** at the **top** (newest first), using the template in that
   file: Goal, Did, Verified (commands/tests + results, or "n/a — no code yet"), Open/blockers, and
   which memory artifacts you updated (linking any ADR from Step 3). Include the sweep's fixes/flags
   from Step 2.

6. **Report** a short summary of what you updated — including what the sweep fixed and anything it
   flagged. Do **not** commit unless the user asks — surface the changed files so they can review. The
   normal expectation is that spine updates ride along with the session's work in the same commit/PR
   (that's what makes the memory *committed*); a separate memory-only commit is fine when the user
   prefers it.

## Guardrails

- **Write surface.** The memory-spine steps (3–5) write **only** to `PROJECT_STATE.md`, `RUN_LOG.md`,
  and `DECISIONS/`. The doc-drift sweep (Step 2) may also edit other docs **but only to correct verified
  drift** per its fix policy — never to add features, restructure, or rewrite content. Never touch code.
- Do not duplicate architecture/scope rules into the spine — those live in the project guide. The spine
  tracks *progress and decisions*, then *points* at the authoritative docs.
- The sweep is a **fast scan, not a full audit.** If it starts ballooning, stop and tell the user a full
  doc audit is warranted as its own task.
- If nothing meaningful changed, it's fine to run a quick sweep, prepend a brief RUN_LOG entry, and skip
  the rest. Don't manufacture state changes.
- A conservative variant: run the sweep **report-only** outside the spine — list drift findings instead
  of fixing them. The default auto-fix policy is deliberate; downgrade only if the project asks for it.

Optional hardening. If checkpoints keep getting skipped, a CI or pre-commit check can enforce the habit — e.g. fail a PR that changes source without touching docs/RUN_LOG.md, or flag a _Last updated:_ date older than the newest RUN_LOG entry. Add this only if drift actually bites; the core system is deliberately automation-light.


3. The protocol & data flow

The pieces are useless without the loop that drives them. Wire this into the project guide (e.g. your CLAUDE.md / AGENTS.md / README) so every agent follows it:

Protocol. At the start of substantive work, read PROJECT_STATE.md and the latest RUN_LOG.md entry. At the end of a session, run the /checkpoint routine: add an ADR if a meaningful decision was made, refresh PROJECT_STATE.md, and prepend a RUN_LOG.md entry. A session-start hook injects git state + the top of PROJECT_STATE.md automatically.

Add a short pointer section to the project guide (don't restate the spine's content — just point at it):

## Project memory — read at session start, update at session end

This repo keeps a **committed, version-controlled memory spine** so progress and decisions survive
across sessions and machines (unlike a tool's machine-local auto-memory). It does **not** restate the
architecture or scope rules — it tracks *what's done and what's been decided*:

- **`docs/PROJECT_STATE.md`** — the one-page live snapshot. **Read this first** when resuming work.
- **`docs/RUN_LOG.md`** — prepend-only session history (what happened, when; newest on top).
- **`docs/DECISIONS/`** — Architecture Decision Records (the *why* behind non-trivial calls).

**Protocol.** Start of work: read `PROJECT_STATE.md` + the latest `RUN_LOG.md` entry. End of session:
run `/checkpoint`. A `SessionStart` hook injects git state + the top of `PROJECT_STATE.md` automatically.

The information-flow rules that keep it coherent

These four rules are the thing that separates a spine that stays useful from a pile of contradictory Markdown:

  1. Three time-horizons, three files, three update rules. NowPROJECT_STATE.md (overwrite). HistoryRUN_LOG.md (prepend new entries on top, never edit old ones). RationaleDECISIONS/ (write once, supersede via new ADR). Don't blur them.
  2. One authority per fact. Architecture/scope/invariants live in the project guide; the spine points at them. If a fact appears in two places and they disagree, the non-authority copy is the bug — fix it into a pointer.
  3. Ground truth comes from code/config/git, never from another doc. The drift sweep verifies claims against the real artifacts, because a stale doc citing a stale doc proves nothing.
  4. Never rewrite history. RUN_LOG entries and ADRs are point-in-time. Superseded? Add a new record (or a dated annotation), don't edit the old one.

4. Adapting it to your project

4.1 Minimum viable version

If the full set feels heavy, this is the smallest version that still delivers the core benefit:

  1. docs/PROJECT_STATE.md (keep Current position / Next actions / Open questions).
  2. docs/RUN_LOG.md (with the entry template).
  3. The protocol pointer in your project guide (read at start, checkpoint at end).

Add DECISIONS/ the first time you make a decision worth recording. Add the session-start hook once the manual "read STATE first" habit proves worth automating. Everything scales up from there.

4.2 What to rename / strip / keep

  • Keep, always: the three-file split and its update rules; the "spine points at, never restates the authority" discipline; the checkpoint routine's doc-drift sweep and ADR-only-if-real-decision rules.
  • Rename to fit your domain: "phase tracker" → "milestone tracker" / "sprint board"; "scope gate" → whatever your release stages are called. The structure (a status-tracked table with a legend) is what matters, not the labels.
  • Strip if not applicable: the scope-gate tracker and scope ledger exist to defend against scope creep on a large, staged design. A small or open-ended project can drop both and just keep the phase/milestone tracker. (But if your project has a "we keep almost adding things" risk, the ledger is the cheapest defense there is — it turns every "should we build X now?" into a logged decision.)

4.3 Mapping the automation to other agents/tools

The Markdown files are universal. Only the hook and the skill packaging are Claude-Code-specific:

  • The session-start hook. The portable idea is: at session start, surface git state + the top of PROJECT_STATE.md. Map it to whatever your tool offers —
    • Claude Code: the SessionStart hook in .claude/settings.json (shown in §2.4).
    • Other agent CLIs / IDE agents: a "session start" / "pre-task" hook if one exists; otherwise a line in the always-loaded guide file (AGENTS.md, GEMINI.md, .cursorrules, CLAUDE.md) instructing the agent to open PROJECT_STATE.md and run git log --oneline -10 before starting.
    • No hook support at all: make "read PROJECT_STATE.md first" the opening line of the project guide. The loop still works; it's just a documented habit instead of automation.
  • The /checkpoint skill. If your tool supports skills/slash-commands/saved-prompts, package the §2.5 body as one. If not, paste the same steps into the project guide under "End of session" and have the agent follow them. The content (sweep → maybe-ADR → STATE → RUN_LOG → report, plus the guardrails) is what does the work, not the packaging.

4.4 Common failure modes (and the rule that prevents each)

Failure modePrevented by
PROJECT_STATE.md grows into a second changelogKeep it ~one page; push detail to RUN_LOG (flow rule 1, §3).
Spine slowly contradicts the README/architecture docs"One authority per fact"; the drift sweep (flow rules 2–3, §3).
ADRs get edited when a decision changes, erasing history"Never rewrite history" — supersede with a new ADR (flow rule 4, §3).
Every trivial change spawns an ADR"ADR only if a real decision with alternatives" (§2.3, checkpoint Step 3).
Docs drift because updates are ad-hocThe /checkpoint routine makes the end-of-session write consistent.
Agent starts cold, re-discovers everythingThe session-start hook + "read STATE first" protocol.

4.5 Working across branches and agents

The spine's hottest files are shared, so a small protocol prevents most of the pain:

  • Checkpoint on the branch you worked on. If the spine moved on the main branch while you worked, rebase/merge first so the checkpoint updates the latest STATE, not a stale one.
  • PROJECT_STATE.md conflicts: it's a snapshot, not a ledger — take the newer session's version wholesale, then re-check that Next actions and the trackers reflect both branches' work.
  • RUN_LOG.md conflicts: trivial by design — keep both entries, newest date first.
  • ADR number races: if two branches both claim ADR-0007, the branch that merges second renumbers its ADR (numbers are cheap and never reused). Keeping the index in sync is what catches this.
  • Concurrent agents: avoid two agents checkpointing the same clone at once. If parallel agents are the norm, have one own the checkpoint and the others hand it their session summaries.

4.6 Setup checklist — the definition of done

When an agent is handed this guide with "set this up for us," done looks like:

  • docs/PROJECT_STATE.md created; every <placeholder> replaced with a real project fact or removed. No leftover template text, no copied EXAMPLE content.
  • Current position, Next actions, and the trackers reflect the actual repo — derived from code, config, git history, and the human's answers. Never invent a project fact to fill a template slot; anything unknown becomes an Open question.
  • docs/RUN_LOG.md created, with a first entry recording the spine setup itself (Verified: n/a — spine scaffold only is fine).
  • docs/DECISIONS/ created with ADR-TEMPLATE.md and the README.md index. ADR-0001 written only if a real architecture baseline exists to record (§2.3.4).
  • The protocol pointer (§3) added to the project guide (CLAUDE.md / AGENTS.md / README).
  • The session-start hook installed and tested (start a fresh session; confirm the injected context appears) — or deliberately skipped per §4.1, with that noted in the RUN_LOG entry.
  • The /checkpoint routine packaged for the tool in use (§4.3) — or its steps pasted into the project guide under "End of session."

5. Worked example: Yuletide Works

EXAMPLE ONLY. This entire section describes the source project this guide was distilled from. Mine it for patterns; do not copy its content into your own spine.

The real project this guide is distilled from is a single-player isometric factory-automation game built in Unity (C#), with a hard rule that the simulation is the truth and Unity is only the representation. It's a useful example precisely because it's large, staged, and at high risk of scope creep — exactly where a memory spine earns its keep. The spine maps to it like this:

  • PROJECT_STATE.md — a 16-row phase tracker (Phase 0 foundation → Phase 15 MVP review gate), a four-gate scope-gate tracker (Prototype → MVP → Vertical slice → Version 1), and a scope ledger listing 13 features hard-deferred to "Version 1" (magical power, fluids, recycling, multi-floor workshop, full elf village, …). When a session is tempted to build a deferred feature early, it adds a ledger row with the rationale instead of building it. The _Last updated:_ line is a paragraph that names the last merge and what's next, so the auto-injected head reads as a status report on its own.
  • RUN_LOG.md — one entry per session, newest on top, each carrying real verification (compile 0 errors, EditMode 55/55, PlayMode 5/5, user visual confirm passed) and Memory updated: checkboxes.
  • DECISIONS/ — six ADRs: architecture baseline (0001), test framework (0002), save format (0003), Windows-build verification (0004), Unity version + scaffold (0005), and a per-phase design ADR (0006). 0005 supersedes the engine-version section of 0001 while the rest of 0001 stands — handled by a "Lifecycle note" in the index, not by editing 0001. ADRs move Accepted → Enacted when the code lands.
  • session-start hook.claude/hooks/session-preflight.sh prints git state, the last 10 commits, and the top 60 lines of PROJECT_STATE.md, wrapped as additionalContext JSON for Claude Code's SessionStart hook.
  • /checkpoint skill.claude/skills/checkpoint/SKILL.md, run at the end of every session, doing the doc-drift sweep then the maybe-ADR / STATE / RUN_LOG writes.

The project guide (CLAUDE.md) carries the protocol pointer and the cardinal rules; the spine points at it and never restates them. That separation — authority in the guide, progress and decisions in the spine — is the whole trick.


This guide is self-contained: the templates in §2 are everything you need to stand the system up. Start with the minimum viable version (§4.1) and grow it as the project does.