gate: instructions_gate check 6 — the auto-memory index (R-229)
gates / gates (push) Successful in 15s
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:
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user