memory-factcheck · git:20261024.a835af9 · 2026-10-24 · sha256 6c8ad6538215c1c7
memory-factcheck git:20261024.a835af9A
Immutable. This exact content never changes and is served at /api/v1/blob/6c8ad6538215c1c7.
---
name: memory-factcheck
description: An audit skill that checks an agent's persistent memory against ground truth (code, database, issue tracker, filesystem), corrects stale memories, and reports dead ones as archive candidates. Use when memory grows past ~30 files, right after a big stack/infra change (library swap, version upgrade, server migration, schema drop), when two memories seem to contradict each other, or on a "clean up / audit my memory" (메모리 정리해줘 / 감사해줘) request.
---
# Memory Fact-Check
Memory decays. A fact that was true when written becomes false once code, schema and
infrastructure move. **Stale memory is worse than no memory** — an agent reads it and
confidently does the wrong thing.
This skill is **not** a structural cleanup. Orphan files, duplicate entries, index bloat,
broken internal links — those checks compare memory files *to each other*. This skill
compares each memory **to the world it describes**: the code, the database, the issue
tracker, the filesystem. A memory can be perfectly formatted, correctly indexed and recently
modified — and its content completely false.
Automatic deletion is forbidden. Only a source-of-truth comparison can tell what is dead, and
losing a single incident lesson is expensive. Hence semi-automatic: **correct freely, archive
only after approval, never delete.**
## 1. Locate and inventory
Find the memory set. Common locations, in priority order:
- The path declared by the project's agent instructions (`AGENTS.md` / `CLAUDE.md`) — e.g.
"memory SSOT is `.claude/memory/`". A declared path wins over every default.
- `.claude/memory/` inside the repo (team-shared, committed to the repo)
- The host's per-project memory directory (e.g. `~/.claude/projects/<slug>/memory/`)
For each file, collect the frontmatter (`name`/`description`/`type`) and the last modification
date (`git log -1 --format=%cs -- <file>` for committed memory, `stat` otherwise). If there is
an index file (`MEMORY.md`), note it — it is audited too, but it is an index, not a memory.
**Personal files are out of scope** — anything the project marks as personal (`user_*.md`,
etc.) belongs to its owner. Do not touch it.
**Reading many files**: dumping 50 memories into context at once exceeds the tool output cap
and wastes the budget. Concatenate them into one scratch file with `########## <filename>`
headers, then read it page by page. Do not skim — a stale claim is usually one clause inside an
otherwise sound paragraph.
### Pre-check script
Running the script below once during the inventory step surfaces structural problems and
claim candidates.
```bash
python3 <skill-path>/scripts/memory_audit.py <memory-dir> --repo <repo-root>
```
- There are five kinds of structural problems. A file the index points to is missing, a file is
missing from the index, and frontmatter is broken. A `[[link]]` pointing to a nonexistent
memory and a duplicate `name` are counted too. If any one exists, the exit code is 1.
- `type` is accepted both at the top level and under `metadata.type`. Real memories are often
stored the latter way.
- This script does not create, modify or delete files. Corrections and archiving happen after
human approval, following the procedure below.
- It extracts file paths, function names and issue numbers as claim candidates and checks them
against the filesystem of `--repo`. Text inside code blocks, globs, URLs and options are
excluded from the candidates.
- Claims that could not reach their source come out as unverified. Issue numbers are always
unverified, since the tracker is not queried. A memory with no claims at all is not fresh
either.
- If the memory directory or the `--repo` path does not exist, it ends with exit 2.
- This is for narrowing down candidates. Choosing which claims change behavior and checking
them against the source is still the agent's job.
## 2. Extract load-bearing claims
Per file, pick the **1–3 claims that change what an agent would do (load-bearing claims)**.
Ignore narrative, rationale and background; a memory is only as stale as its actionable
claims.
Load-bearing claims look like: "X lives at path P" · "table T has N rows" · "issue #N is still
open" · "feature F does not exist yet" · "library L is not installed" · "this fix is still
pending" · "run command C to verify".
## 3. Verify against the source of truth
Work **cheapest-and-highest-yield first**. In practice the ranking below holds: issue status is
one API call and catches the largest share of stale claims — because memories are written
mid-work, and when the work finishes nobody goes back to fix the memory.
| Order | Claim type | How to verify |
| --- | --- | --- |
| 1 | **Issue/PR state** ("#N open", "waiting on #N", "decision pending") | forge CLI/API — `gh issue view N --json state` / `glab api projects/<enc>/issues/N`. Batch them in one loop |
| 2 | **Path/URL** (script locations, deploy paths, endpoints) | `ls`, `test -f`, `curl -s -o /dev/null -w '%{http_code}'` |
| 3 | **Code** (a file/class/config exists, a specific behavior) | `grep`/`Read` the current tree — *the source is the code, not the memory* |
| 4 | **Data/schema** (tables, columns, row counts) | Read-only queries via the project's DB tool. Prefer catalog estimates (`pg_class.reltuples`, `information_schema.columns`); exact `count(*)` only when the estimate itself is disputed |
| 5 | **Runtime/host** (cron jobs, services, logs) | `ssh <host> 'ls …; crontab -l; tail <log>'` — a job's last log line dates the claim precisely |
Parallelize independent verifications. If a source is unreachable, state it in the report —
never silently downgrade "couldn't check" to "checked".
## 4. Classify
- **fresh** — every claim holds. Don't touch it.
- **stale** — some claim is outdated (moved path, changed number, closed issue, implemented
gap). → **Correct the body now, with the measured value and the date.** Corrections are
within autonomous scope, because they add truth rather than delete anything.
- **dead** — the core premise is gone (library removed, feature retired, wholly superseded).
→ Mark it as an archive **candidate** only.
- **unverified** — the claim could not be confirmed because the source was unreachable (auth
failure, host unreachable, no permission). → Leave it untouched, but count it separately in
the report and note what blocked it. Do not count it as fresh.
## 5. Stale patterns worth hunting
Beyond "the number changed", these recur and are easy to miss:
- **Fixed-gap drift** — the memory records a missing capability ("no reconcile on startup",
"no rate limiting yet") and it has since been implemented. This is the most dangerous class:
the agent re-implements or re-reports work that has already shipped. Check the linked issue
*and* grep for the symbol together.
- **Cross-memory contradiction** — two memories say different things (one says script X is the
canonical way, another says that script was retired). By definition at least one is stale.
Do not look only file by file; cross-compare claims across files.
- **Scale drift** — "table T has ~8M rows" from a few months ago is now off by 25%. Harmless as
small talk, harmful when the memory derives advice from that number (batch sizes, timeout
budgets, "this query takes 19s").
- **Recipe rot** — a command/query the memory saved as a verified recipe no longer works at
today's data volume or API version. **Re-run saved recipes**; a recipe you did not run is not
verified.
- **Progress-state drift** — the "current status" section of a long-running job/backfill memory
falls weeks behind. Sometimes two mutually contradictory status sections pile up. Date each
section, keep only the newest, and mark the superseded ones.
- **Identity mismatch** — `name`/`description` and the body say opposite things (e.g. a file
named `*-via-toolX` whose body records that toolX was abandoned). Recall matches on the
description, so the file gets loaded for the wrong reason or missed entirely.
## 6. Report, then apply
Report as a table before changing anything — file · class · one-line evidence · action:
| File | Class | Evidence | Action |
| --- | --- | --- | --- |
| `reference_x.md` | stale | script moved `scripts/` → `data/` | path corrected |
| `project_y.md` | stale | claims "#302 reconcile missing"; `JobRunHistoryReconciler` exists, #302 closed | rewritten as done |
| `project_z.md` | dead candidate | the feature from #N was removed in #M | awaiting approval |
Then:
1. **Apply corrections to stale bodies** — measured value + date, keeping the original
observation where it still carries a lesson ("8M as of <date>, 9.9M as of <today>").
2. **Archive dead candidates only after explicit user approval**: `git mv` them into
`<memory>/archive/` and add `archived: <date> <reason>` to the frontmatter. Never use `rm`.
3. **Sync the index** — reflect the corrections and remove archived entries from `MEMORY.md`.
4. **Commit through the project's normal workflow** (issue → branch/worktree → PR/MR). Memory
is team-shared content; there are no direct-to-main commits.
## Judgment rules — stay conservative
- **Unverifiable ⇒ unverified.** If the source of truth is unreachable, leave the memory as it
is and classify it as unverified. Unconfirmed is not dead, but it is not confirmed either. If
you count it as fresh, the next audit skips that memory.
- **Incident lessons stay fresh even when the code moves.** A memory recording *why something
broke* exists to prevent recurrence, not as a snapshot of a call site. Correct its stale path
references, but do not retire the lesson because a file was renamed.
- **Report the drift, don't invent the cause.** If a number inverted or moved without
explanation, record the measured value and mark it as unexplained. A plausible story written
into memory becomes tomorrow's false fact.
- **Prefer merge over create.** Two memories on one topic → propose merging into the existing
one.
- **Non-obvious facts discovered during the audit become new memories** — the audit itself is a
source of evidence.
## Field notes
- A recent timestamp proves nothing. A file committed this week can carry a claim that was
already false when written, and a file left untouched for months can still be true. Verify
the claims — do not sort by date and cut off the tail.
- `count(*)` on a large table can exceed the DB tool's statement timeout — if the memory claims
"this query is fast", that failure is itself a finding.
- When shelling out from zsh, quote globs (`grep --include="*.java"`). Otherwise the shell eats
them, the check silently returns an empty result — and you get a false "fresh".
- An issue being closed does not by itself prove that the described work shipped. Confirm
fixed-gap claims with a grep for the symbol as well.