gate: instructions_gate check 6 — the auto-memory index (R-229)
gates / gates (push) Successful in 15s

MEMORY.md is the larger half of what loads before a word is typed (8.4k tokens vs the root
CLAUDE.md's 6.6k) and is the one instruction file nobody hand-edits, so nothing was watching it.

Three deliberately different outcomes, each pinned by a test: over-ceiling FAILS (auto-memory
drops content past the limit with no error), an orphan WARNS (the store is outside git), and an
absent store PASSES while PRINTING its reason -- asserted on the reason text, because a pass with
no reason is indistinguishable from a gate that stopped running.

39 assertions (was 20). Red-proof run against the real store, not a fixture.
This commit is contained in:
2026-08-06 10:48:59 +02:00
parent 3a9dd81e18
commit f27aed87cd
3 changed files with 265 additions and 1 deletions
+126 -1
View File
@@ -39,6 +39,26 @@ CHECKS (each names the file and the reason; a missing input is a FAILURE, never
felhom.eu/documentation/runbooks/workspace-CLAUDE.md are byte-identical. The live file sits in
a directory that is not a git repo, so the copy is the only version-controlled record of it;
nothing but this check enforces that they agree.
6. The auto-memory index (<workspace>/.claude-memory/MEMORY.md) is within its line and byte
limits, and every top-level topic file is referenced by it.
WHY 6 EXISTS, AND WHY ITS THREE OUTCOMES DIFFER (2026-08-06).
MEMORY.md is the LARGER half of what loads before a word is typed: measured at the workspace root,
the hand-written root CLAUDE.md was 6.6k tokens and MEMORY.md 8.4k. It is also the one instruction
file nobody hand-edits — Claude writes it — so nothing was watching it.
- OVER THE LIMIT IS A FAILURE. Content past the auto-memory limit is dropped with NO error. A
silent truncation of the index is the failure mode with no observable at all, which is exactly
the class this project keeps getting bitten by.
- AN ORPHAN IS A WARNING, NOT A FAILURE. The store changes between sessions and lives outside git;
a hard fail would block pushes for something no commit can fix. On 2026-08-06 the index
referenced 113 files while 157 existed — 44 held knowledge nothing would ever read.
- AN ABSENT STORE PASSES, AND SAYS SO OUT LOUD. This is a DELIBERATE exception to check 1's
"a missing input is a FAILURE, never a skip": the store is machine-local by design and a clone
on any other host legitimately has none. The reason is printed in the tally so an absent store
can never be mistaken for a silent skip — which is the only thing that made the exception safe
to grant.
THE POSITIVE OBSERVABLE. Every root prints a per-check tally with the measured numbers, not just a
verdict. "0 failures" alone cannot tell a working gate from a blind one — if the effective line
@@ -53,6 +73,14 @@ import sys
MAX_LINES = 200
# The auto-memory index. Limits are the store's own, not this project's taste: content past them is
# dropped with no error.
MEMORY_DIRNAME = ".claude-memory"
MEMORY_INDEX = "MEMORY.md"
MEMORY_ARCHIVE = "archive"
MAX_MEMORY_LINES = 200
MAX_MEMORY_BYTES = 25 * 1024
# Block-level HTML comments: stripped before injection, so they cost nothing and are not counted.
COMMENT_RE = re.compile(r"<!--.*?-->", re.S)
@@ -189,6 +217,96 @@ def check_workspace_copy(workspace_root, failures, tally):
)
def memory_referenced(index_text):
"""Basenames the index points at, via [](x.md) links and [[wikilink]]s alike.
Both forms count as a reference. A file reachable only by a wikilink is indexed — it just is not
a listed entry — and calling it an orphan would push someone to add a duplicate row for it.
"""
md = re.findall(r"\]\(([^)]+\.md)\)", index_text)
wiki = re.findall(r"\[\[([^\]]+)\]\]", index_text)
names = set(os.path.basename(x) for x in md)
for w in wiki:
names.add(os.path.basename(w if w.endswith(".md") else w + ".md"))
return names
def check_memory(workspace_root, failures, warnings, tally):
store = os.path.join(workspace_root, MEMORY_DIRNAME)
if not os.path.isdir(store):
# DELIBERATE exception to "a missing input is a FAILURE" — see the module docstring. The
# reason is printed so this can never read as a silent skip.
tally.append(
" memory index : absent (%s/ not on this host — the auto-memory store is "
"machine-local by design, so a clone elsewhere legitimately has none; PASS with reason)"
% MEMORY_DIRNAME
)
return
index = os.path.join(store, MEMORY_INDEX)
if not os.path.exists(index):
failures.append(
"%s: the memory store exists but has no %s. An un-indexed store is unreachable "
"knowledge — every topic file is read on demand, and the index is the only thing that "
"names them." % (store, MEMORY_INDEX)
)
tally.append(" memory index : MISSING")
return
with io_open(index) as fh:
text = fh.read()
nlines = text.count("\n")
nbytes = len(text.encode("utf-8"))
tally.append(
" memory index : %d lines (ceiling %d), %d bytes (ceiling %d)"
% (nlines, MAX_MEMORY_LINES, nbytes, MAX_MEMORY_BYTES)
)
if nlines > MAX_MEMORY_LINES:
failures.append(
"%s: %d lines, ceiling %d. Content past the auto-memory limit is DROPPED WITH NO "
"ERROR — a truncated index is a silent failure with no observable. Move detail out of "
"the index into the topic files it points at; the index is what loads, the topic files "
"are read on demand." % (index, nlines, MAX_MEMORY_LINES)
)
if nbytes > MAX_MEMORY_BYTES:
failures.append(
"%s: %d bytes, ceiling %d. Same reason as the line ceiling — silent truncation. Move "
"detail into topic files rather than dropping entries."
% (index, nbytes, MAX_MEMORY_BYTES)
)
referenced = memory_referenced(text)
on_disk = set(
n for n in os.listdir(store)
if n.endswith(".md") and n != MEMORY_INDEX
and os.path.isfile(os.path.join(store, n))
)
orphans = sorted(on_disk - referenced)
archived = 0
arc = os.path.join(store, MEMORY_ARCHIVE)
if os.path.isdir(arc):
archived = len([n for n in os.listdir(arc) if n.endswith(".md")])
tally.append(
" memory topic files : %d indexed, %d orphaned, %d archived"
% (len(on_disk) - len(orphans), len(orphans), archived)
)
if orphans:
# WARN, never FAIL: the store is outside git and changes between sessions, so a failure
# here would block pushes for something no commit can fix.
warnings.append(
"%s: %d top-level topic file(s) are not referenced by %s, so nothing will ever read "
"them: %s%s"
% (
store,
len(orphans),
MEMORY_INDEX,
", ".join(orphans[:8]),
(" (+%d more)" % (len(orphans) - 8)) if len(orphans) > 8 else "",
)
)
def io_open(path):
return open(path, "r", encoding="utf-8")
@@ -200,6 +318,7 @@ def main(argv):
return 2
failures = []
warnings = []
for root in roots:
root = os.path.abspath(root)
print("instructions_gate: %s" % root)
@@ -218,10 +337,16 @@ def main(argv):
check_rules(root, failures, tally)
check_workspace_copy(os.path.dirname(root), failures, tally)
check_memory(os.path.dirname(root), failures, warnings, tally)
for line in tally:
print(line)
if warnings:
print("")
for w in warnings:
print("WARNING: %s" % w)
if failures:
print("")
print("instructions_gate: %d FAILURE(S)" % len(failures))
@@ -230,7 +355,7 @@ def main(argv):
return 1
print("")
print("instructions_gate: OK")
print("instructions_gate: OK%s" % (" (%d warning(s))" % len(warnings) if warnings else ""))
return 0