Skip to content

Jonwhitefang.uk

Stardate 2026.191 · systems nominal
All prompts
Agent documentUpdated 2026-07-07

Portable Memory Spine

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

Source: JonWhiteFang/Documents · guides/PORTABLE_MEMORY_SPINE_GUIDE.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."


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         (append-only history)  │
                    │   • DECISIONS/*.md     (ADRs — the "why")     │
                    └───────────────┬───────────────────────────────┘
                                    │
        START of session           │            END of session
   ┌────────────────────┐          │          ┌──────────────────────┐
   │ session-start hook │ ── reads ┘          │  /checkpoint routine │
   │ injects git state  │                     │  doc-drift sweep +   │
   │ + top of STATE     │ ── writes ─────────▶│  updates STATE,      │
   └────────────────────┘                     │  appends RUN_LOG,    │
                                               │  adds ADR if needed  │
                                               └──────────────────────┘

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?"Append-only — newest entry on top; 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 (or your project's docs home) and a docs/DECISIONS/ subdirectory. 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>.

| # | 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. Resolve each via an ADR when decided, then mark it RESOLVED with a link.

- **<question>** — <why it's open / what's blocked on it>. → ADR 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 — append-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 append-only: never rewrite past entries (they are point-in-time records).

# Run Log — <Project Name>

> Append-only history of working sessions. Newest entry at the **top**. 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

# ADR-NNNN: <decision title>

- **Status:** Proposed | Accepted | Enacted | Superseded by ADR-XXXX | Deprecated
- **Date:** YYYY-MM-DD
- **Deciders:** <who>
- **Phase / stage:** <e.g. Phase 0, MVP>

## 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.

## 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.

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 context, decision, status, date, consequences. Add a **Relations** line
   if it supersedes or pairs with an existing ADR — and update the other ADR's status to point back.
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.

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) and git. 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) updates PROJECT_STATE.md, (c) appends a RUN_LOG.md entry, and (d) adds an ADR only if a real decision was made. 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), updates docs/PROJECT_STATE.md, appends docs/RUN_LOG.md, and adds an ADR if a non-trivial
  decision was made.
---

# 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, **annotate** it with a dated
     pointer to what superseded it; don't edit the original decision.
   - **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 4).

3. **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**.
   - If you were tempted toward an out-of-scope item and deferred it, add a row to the **scope ledger**.

4. **Append 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. Include the sweep's fixes/flags from Step 2.

5. **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 link it from `PROJECT_STATE.md` → References and the RUN_LOG entry.

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.

## 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, append a brief RUN_LOG entry, and skip
  the rest. Don't manufacture state changes.

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: refresh PROJECT_STATE.md, append a RUN_LOG.md entry, and add an ADR if a meaningful decision was made. 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`** — append-only session history (what happened, when).
- **`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 (append, never edit). 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 → STATE → RUN_LOG → maybe-ADR → 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 (rule §3.1).
Spine slowly contradicts the README/architecture docs"One authority per fact"; the drift sweep (rules §3.2–3.3).
ADRs get edited when a decision changes, erasing history"Never rewrite history" — supersede with a new ADR (rule §3.4).
Every trivial change spawns an ADR"ADR only if a real decision with alternatives" (§2.3, checkpoint Step 5).
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.

5. Worked example: Yuletide Works

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 STATE / RUN_LOG / maybe-ADR 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.