Files
felhom.eu/scripts/instructions_gate.py
T
admin 15fa5273ba
gates / gates (push) Successful in 8s
gate: check 7 (register citations) + content WARNings on the memory index
Check 7 catches "cites a register item and calls it open when it is not" -- the R-168 class, four
files, one self-contradicting. Trigger is an openness CLAIM, not any citation: policing every
mention would fire on ~30 legitimate provenance citations and the gate would be switched off.
Deliberate deviation from the task's literal wording, to keep it alive.

Two bugs found by the check's own red-proofs, both of which would have shipped:
- the state marker is not self-closing (**SHIPPED - text**), so the first parser read R-168 itself
  as OPEN -- a gate that cannot convict its founding case is decoration;
- the CLOSED exemption was line-wide, so "shipped" in a title pardoned "OPEN R-25b".

Check 6 gains WARN-only content classes on MEMORY.md. Link targets are stripped first: the earlier
scan reported three expired statements, all three false (dates in filenames), while missing the one
real expired claim, whose deadline was written ~08-02 with no ISO date.

39 -> 60 assertions. All four runners green.
2026-08-06 11:39:34 +02:00

605 lines
28 KiB
Python

# -*- coding: utf-8 -*-
"""Instruction-file consistency gate — keeps CLAUDE.md files from silently regrowing.
Usage: python3 scripts/instructions_gate.py <repo-root> [<repo-root> ...]
python3 scripts/instructions_gate.py --fast <repo-root> (identical: no network,
no container runtime)
THIS IS A CONSISTENCY GATE, NOT A BUDGET GATE. Measured 2026-08-06 on a 1M-token window: the
instruction files occupied 15k tokens against 869k free. Space is NOT the constraint here and no
failure message may claim it is. The line ceiling exists because longer instruction files reduce
ADHERENCE — Anthropic's guidance is to keep a CLAUDE.md under 200 lines — and because a file nobody
can hold in their head is where contradictions hide. Four were found in this project on 2026-08-06,
two of which decided where a destructive drill runs.
WHAT IS COUNTED. Every check runs against EFFECTIVE text: block-level HTML comments are stripped
first, because they are stripped before injection and never reach the model. Verified empirically on
Claude Code 2.1.222 with a control (two plain markers -> both seen) and a treatment (one marker
inside <!-- -->, twice -> not seen). That is the whole point of the comment convention: earned
rationale stays in the repo for human readers at zero cost to the instructions.
CHECKS (each names the file and the reason; a missing input is a FAILURE, never a skip):
1. <root>/CLAUDE.md is at most MAX_LINES effective lines.
2. Every <root>/.claude/rules/*.md declares `paths:` in frontmatter, or `unconditional: true`.
NOTE: `paths:` is a Claude Code feature — a rule carrying it loads only when a file matching
one of its globs is read. `unconditional: true` is NOT a product feature; it is OUR marker,
asserting that always-loading was deliberate. The product loads a rule with no `paths:` key
unconditionally either way, so this check catches the ACCIDENT, not the product behaviour.
3. No component version literal in effective text. Versions change several times a day and the
fleet is not uniform, so a version in an instruction file is stale within a day (ask the hub's
/hosts + /configs, or the box). Historical citations belong in an HTML comment beside the rule
they justify, where they inform a human and cannot go stale in the model's view.
IPv4 addresses are NOT versions — a bare \\d+\\.\\d+\\.\\d+ matches the first three octets of
every one of them, which is how this check would otherwise fail on its own repo.
4. No TEMPORARY block carrying a date already past. The block this gate was written for said
"TEMPORARY - until ~2026-08-02 ... Delete this block on return" and was still being read as
current fact on 2026-08-06, while a sibling repo's CLAUDE.md asserted the opposite.
5. The workspace-root CLAUDE.md and its versioned copy at
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, every top-level topic file is referenced by it, and its CONTENT is warned about
(version literals, host addresses, expired statements, stale-open register citations).
7. Every R-nnn cited in a CLAUDE.md or a .claude/rules/*.md that is CALLED OPEN really is open.
WHY 7 EXISTS (2026-08-06). Four instruction files asserted that CI was still owed, citing R-168,
four days after it closed — and one of the four contradicted itself, its release section already
saying R-168 mails the failure. A trim is a VOLUME operation: it does not validate content, and the
sentence survived because it read as settled. This class, at least, is mechanical.
- THE TRIGGER IS AN OPENNESS CLAIM, NOT ANY CITATION. Requiring every mention of a non-open item
to carry "CLOSED" would fire on ~30 legitimate provenance citations — "(R-161)", "R-117 spike
§6.3" — which cite an item as the SOURCE OF A FACT, not as outstanding work. A gate that noisy
is switched off, which is the fate R-29 documented.
- THE EXEMPTION IS SCOPED TO THE CITATION'S CLAUSE. A line-wide test pardoned a stale claim
because the words "shipped" appeared elsewhere on the same line, in a title and a filename.
- A CITATION OF AN ITEM IN NEITHER REGISTER FAILS. A reference to nothing cannot be checked by
anyone, which is worse than a stale one.
- THE STATE MARKER IS NOT SELF-CLOSING. Real rows read `**SHIPPED — and the alarm is
DEMONSTRATED**`. An earlier parser required `**SHIPPED**` and therefore read R-168 — the row
this check exists for — as open. A gate that cannot convict its own founding case is decoration.
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
count suddenly reads 3, the stripper broke, and the count is where you see it.
The kill condition is pinned by scripts/test_instructions_gate.py: an over-length CLAUDE.md still
FAILS, and a rule file with neither marker still FAILS.
"""
import os
import re
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)
# A semver-ish literal that is NOT part of a dotted quad (IPv4) and not part of a longer run.
VERSION_RE = re.compile(r"(?<![\d.])\d+\.\d+\.\d+(?![\d.])")
# "TEMPORARY" anywhere on a line, plus an ISO date somewhere in that block.
TEMPORARY_RE = re.compile(r"TEMPORARY")
ISO_DATE_RE = re.compile(r"(\d{4})-(\d{2})-(\d{2})")
WORKSPACE_COPY = os.path.join(
"felhom.eu", "documentation", "runbooks", "workspace-CLAUDE.md"
)
def effective(text):
"""The text the model actually receives: HTML comments removed."""
return COMMENT_RE.sub("", text)
def today_tuple():
"""Local date as (y, m, d). Injectable via FELHOM_GATE_TODAY for the test suite."""
override = os.environ.get("FELHOM_GATE_TODAY")
if override:
m = ISO_DATE_RE.match(override.strip())
if m:
return tuple(int(g) for g in m.groups())
import datetime
d = datetime.date.today()
return (d.year, d.month, d.day)
def check_length(path, failures, tally):
with io_open(path) as fh:
eff = effective(fh.read())
n = eff.count("\n")
tally.append(" CLAUDE.md effective lines : %d (ceiling %d)" % (n, MAX_LINES))
if n > MAX_LINES:
failures.append(
"%s: %d effective lines, ceiling %d. This is an ADHERENCE limit, not a space "
"limit — long instruction files get followed less reliably and hide "
"contradictions. Move path-bound guidance into .claude/rules/*.md with a `paths:` "
"list, procedures into the skill that already covers them, and earned rationale "
"into an HTML comment (free: stripped before injection)."
% (path, n, MAX_LINES)
)
def check_rules(root, failures, tally):
rules_dir = os.path.join(root, ".claude", "rules")
if not os.path.isdir(rules_dir):
tally.append(" rule files : none (no .claude/rules/)")
return
names = sorted(n for n in os.listdir(rules_dir) if n.endswith(".md"))
scoped = 0
for name in names:
path = os.path.join(rules_dir, name)
with io_open(path) as fh:
head = fh.read(2048)
has_paths = re.search(r"^paths:", head, re.M) is not None
has_uncond = re.search(r"^unconditional:\s*true\s*$", head, re.M) is not None
if has_paths:
scoped += 1
elif not has_uncond:
failures.append(
"%s: rule file has neither a `paths:` frontmatter list nor an explicit "
"`unconditional: true`. Without `paths:` Claude Code loads it in EVERY session, "
"which is rarely what a rule file is for — add the globs it applies to, or "
"declare `unconditional: true` to say the always-loading is deliberate." % path
)
tally.append(
" rule files : %d (%d path-scoped)" % (len(names), scoped)
)
def check_versions(path, failures, tally):
with io_open(path) as fh:
eff = effective(fh.read())
hits = []
for i, line in enumerate(eff.split("\n"), 1):
for m in VERSION_RE.finditer(line):
hits.append((i, m.group(), line.strip()[:90]))
tally.append(" version literals : %d" % len(hits))
for lineno, ver, ctx in hits:
failures.append(
"%s:%d: component version literal %r — versions change several times a day and the "
"fleet is not uniform, so this is stale within a day. Ask the hub (/hosts, /configs) "
"or the box. A historical citation belongs in an HTML comment beside the rule it "
"justifies.\n %s" % (path, lineno, ver, ctx)
)
def check_temporary(path, failures, tally):
with io_open(path) as fh:
eff = effective(fh.read())
lines = eff.split("\n")
today = today_tuple()
found = 0
for i, line in enumerate(lines, 1):
if not TEMPORARY_RE.search(line):
continue
found += 1
window = "\n".join(lines[i - 1 : i + 6])
for m in ISO_DATE_RE.finditer(window):
when = tuple(int(g) for g in m.groups())
if when < today:
failures.append(
"%s:%d: TEMPORARY block carrying the past date %s. A temporary block that "
"outlives its own deadline is read as current fact — this gate exists "
"because one did, for four days, while a sibling repo asserted the "
"opposite. Delete it; the audit trail is the record."
% (path, i, m.group())
)
break
tally.append(" TEMPORARY blocks : %d" % found)
def check_workspace_copy(workspace_root, failures, tally):
live = os.path.join(workspace_root, "CLAUDE.md")
copy = os.path.join(workspace_root, WORKSPACE_COPY)
if not os.path.exists(live) or not os.path.exists(copy):
tally.append(" workspace copy : n/a (not this workspace)")
return
with io_open(live) as a, io_open(copy) as b:
same = a.read() == b.read()
tally.append(" workspace copy identical : %s" % ("yes" if same else "NO"))
if not same:
failures.append(
"%s and %s have diverged. The live workspace file sits in a directory that is not a "
"git repo, so the copy is its only version-controlled record — nothing but this "
"check enforces that they agree. Copy the live file over the versioned one."
% (live, copy)
)
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)
)
# --- content classes, as WARNINGS (see memory_content_warnings for why not failures) ---
cw = memory_content_warnings(text, today_tuple())
tally.append(
" memory index content : %d version literal(s), %d host address(es), %d expired "
"statement(s) [WARN only]"
% (len(cw["versions"]), len(cw["addresses"]), len(cw["expired"]))
)
# Fourth class, beyond the three the CLAUDE.md files are failed on: a stale-open register
# citation. Added because TWO of the three false statements found in this file on 2026-08-06
# were exactly this ("R-193 decision open", "OPEN R-25b" — both closed), and both were found by
# hand. Check 7 polices the hand-written files for it; nothing policed this one.
reg = register_state(workspace_root)
if reg:
stale_cites = []
for lineno, raw in enumerate(text.split("\n"), 1):
for item in sorted(set(CITE_RE.findall(raw))):
cl = clause_around(raw, item)
if not (OPENNESS_RE.search(cl) and not SAYS_CLOSED_RE.search(cl)):
continue
if reg.get(item, "missing") != "open":
stale_cites.append((lineno, item, raw.strip()[:90]))
if stale_cites:
tally.append(" memory stale citations : %d [WARN only]" % len(stale_cites))
for lineno, item, ctx in stale_cites[:6]:
warnings.append(
"%s:%d: calls %s open, but the register does not.\n %s"
% (index, lineno, item, ctx)
)
for kind, label, why in (
("expired", "EXPIRED statement", "it is read as current fact"),
("versions", "version literal", "the fleet is not uniform, so it is stale within a day"),
("addresses", "host address", "operations/nodes.md is the single home for these"),
):
for lineno, hit, ctx in cw[kind][:6]:
warnings.append(
"%s:%d: %s %r%s. Not a failure: Claude writes this file between sessions, so "
"this warning is aimed at the model that will next edit it, not at whoever is "
"pushing.\n %s" % (index, lineno, label, hit, why, ctx)
)
if len(cw[kind]) > 6:
warnings.append(
"%s: ... and %d more %s(s) — full list from the tally counts above."
% (index, len(cw[kind]) - 6, label)
)
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 "",
)
)
# A date inside a markdown link TARGET is a filename, not a claim: `[x](vacation-...-2026-07-20.md)`.
# Matching those produced THREE false "expired statement" findings on 2026-08-06 — every one of the
# three reported that day — while missing the one real expired claim in the same file, whose deadline
# was written `~08-02` and carried no ISO date at all. A scan that reports the wrong three and misses
# the right one is worse than no scan.
LINK_TARGET_RE = re.compile(r"\]\([^)]*\)")
# Deadline language, including the bare MM-DD form the real one used.
DEADLINE_RE = re.compile(
r"\b(till|until|by)\s+~?\s*(\d{4}-\d{2}-\d{2}|\d{2}-\d{2})\b", re.I
)
LOOPBACK = ("127.0.0.1", "0.0.0.0", "255.255.255.255")
IPV4_RE = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")
def memory_content_warnings(text, today):
"""The three content classes the CLAUDE.md files are FAILED on, applied to the index as WARNINGS.
WARN and not FAIL, deliberately: Claude writes this file between sessions, so a hard failure
would refuse a human's push over a line no human typed. The warning IS the mechanism — it is read
by the same model that writes the file, which makes the loop self-correcting rather than a chore
for a person.
"""
out = {"versions": [], "addresses": [], "expired": []}
for i, raw in enumerate(text.split("\n"), 1):
# strip link targets before ANY content judgement — a filename is not an assertion
line = LINK_TARGET_RE.sub("]()", raw)
for m in VERSION_RE.finditer(line):
out["versions"].append((i, m.group(), raw.strip()[:90]))
for m in IPV4_RE.finditer(line):
if m.group() not in LOOPBACK:
out["addresses"].append((i, m.group(), raw.strip()[:90]))
for m in DEADLINE_RE.finditer(line):
token = m.group(2)
if len(token) == 5: # MM-DD — assume the current year, which is how it was written
when = (today[0], int(token[:2]), int(token[3:]))
else:
when = tuple(int(x) for x in token.split("-"))
if when < today:
out["expired"].append((i, m.group(0), raw.strip()[:90]))
return out
def register_state(workspace_root):
"""Map R-nnn -> 'open' | 'closed' | 'historical' | 'missing'.
OPEN-ITEMS.md is the authority on what is OPEN and says so in its own header. An item is open
when it OWNS A ROW there whose State cell is not a closed marker. An item with no row of its own
is not open work — it may still be mentioned inside another row, and it is usually in ROADMAP.md,
which keeps the full history. Anything in neither file is a reference to nothing.
"""
base = os.path.join(workspace_root, "felhom.eu", "documentation", "backlog")
reg = os.path.join(base, "OPEN-ITEMS.md")
hist = os.path.join(base, "ROADMAP.md")
if not os.path.exists(reg):
return None
with io_open(reg) as fh:
reg_text = fh.read()
hist_text = ""
if os.path.exists(hist):
with io_open(hist) as fh:
hist_text = fh.read()
state = {}
for line in reg_text.split("\n"):
m = re.match(r"\|\s*\*\*(R-\d+[a-z]?)\*\*\s*\|", line)
if not m:
continue
cells = [c.strip() for c in line.rstrip().strip("|").split("|")]
# The State column is index 2 in BOTH table shapes here (`ID|What|State` and
# `ID|What|State|Blocked on|Next action|Owner`). Scan from there rather than taking the
# last cell: a row whose prose contains a `|` splits into extra cells and would otherwise
# be read from the wrong one.
#
# THE MARKER IS NOT SELF-CLOSING. Real rows read `**SHIPPED — and the alarm is
# DEMONSTRATED**`, not `**SHIPPED**`. An earlier version of this parser required the
# closing `**` immediately after the word and therefore read R-168 — the row this whole
# check exists for — as OPEN. A gate that cannot convict its own founding case is decoration.
state[m.group(1)] = "open"
for cell in cells[2:]:
if not cell.startswith("**"):
continue
head = cell[: cell.find("**", 2) + 2] if cell.find("**", 2) > 0 else cell
if re.search(r"\b(CLOSED|SHIPPED|DONE|COMPLETE)\b", head) and not re.search(
r"\bopen\b", head, re.I
):
state[m.group(1)] = "closed"
break
mentioned = set(re.findall(r"\bR-\d+[a-z]?\b", reg_text)) | set(
re.findall(r"\bR-\d+[a-z]?\b", hist_text)
)
for item in mentioned:
state.setdefault(item, "historical")
return state
# Language that CLAIMS an item is still outstanding. The check fires on these, not on every
# citation — see check 7's rationale in the module docstring.
OPENNESS_RE = re.compile(
r"\b(still owed|is owed|are owed|still open|stays open|remains open|still pending|"
r"pending\b|still needed|not yet|awaiting|awaits|outstanding|unresolved|to be done|"
r"OPEN\s+R-\d)", re.I
)
SAYS_CLOSED_RE = re.compile(r"\b(CLOSED|DONE|SHIPPED|RESOLVED|COMPLETE)\b", re.I)
CITE_RE = re.compile(r"\bR-\d+[a-z]?\b")
# Clause boundaries. The exemption is scoped to the clause holding the citation, NOT the whole line:
# `- [Customer RESET shipped](…) — …; OPEN R-25b` contains "shipped" twice, in a title and a
# filename, and a line-wide exemption pardoned the stale claim at the end of it. Found by this
# check's own red-proof, which failed to go red.
CLAUSE_SPLIT_RE = re.compile(r"[;.]\s+|\s+—\s+|\s+\|\s+")
def clause_around(line, item):
"""The clause of `line` containing `item` — what "the same sentence" means for the exemption."""
for part in CLAUSE_SPLIT_RE.split(line):
if re.search(r"\b%s\b" % re.escape(item), part):
return part
return line
def check_citations(root, workspace_root, failures, warnings, tally):
"""Check 7 — a citation that calls a register item OPEN must be right.
Scope: <root>/CLAUDE.md and <root>/.claude/rules/*.md. Effective text only, so a citation
parked in an HTML comment for history is not policed.
"""
state = register_state(workspace_root)
if state is None:
tally.append(" register citations : n/a (no OPEN-ITEMS.md — not this workspace)")
return
targets = []
claude_md = os.path.join(root, "CLAUDE.md")
if os.path.exists(claude_md):
targets.append(claude_md)
rules_dir = os.path.join(root, ".claude", "rules")
if os.path.isdir(rules_dir):
targets += [
os.path.join(rules_dir, n) for n in sorted(os.listdir(rules_dir)) if n.endswith(".md")
]
cited = 0
for path in targets:
with io_open(path) as fh:
eff = effective(fh.read())
for lineno, line in enumerate(eff.split("\n"), 1):
items = CITE_RE.findall(line)
if not items:
continue
cited += len(items)
for item in sorted(set(items)):
cl = clause_around(line, item)
claims_open = bool(OPENNESS_RE.search(cl)) and not SAYS_CLOSED_RE.search(cl)
st = state.get(item, "missing")
if st == "missing":
failures.append(
"%s:%d: cites %s, which appears in NEITHER OPEN-ITEMS.md nor ROADMAP.md. "
"A reference to nothing is worse than a stale one — it cannot be checked "
"by anyone.\n %s" % (path, lineno, item, line.strip()[:110])
)
elif claims_open and st != "open":
failures.append(
"%s:%d: claims %s is still open, but the register says it is %s. Four "
"instruction files asserted CI was still owed (R-168) four days after it "
"closed, and one contradicted itself about it — a trim is a volume "
"operation and does not validate content. Either drop the claim or say "
"CLOSED in the same sentence.\n %s"
% (path, lineno, item, "not an open row" if st == "historical" else st,
line.strip()[:110])
)
tally.append(" register citations : %d cited, %d register items known" % (cited, len(state)))
def io_open(path):
return open(path, "r", encoding="utf-8")
def main(argv):
roots = [a for a in argv[1:] if a != "--fast"]
if not roots:
sys.stderr.write("usage: instructions_gate.py [--fast] <repo-root> ...\n")
return 2
failures = []
warnings = []
for root in roots:
root = os.path.abspath(root)
print("instructions_gate: %s" % root)
tally = []
claude_md = os.path.join(root, "CLAUDE.md")
if not os.path.exists(claude_md):
failures.append(
"%s: no CLAUDE.md. A missing input is a FAILURE, never a skip — a gate that "
"silently passes on an absent file is how the check stops running." % root
)
else:
check_length(claude_md, failures, tally)
check_versions(claude_md, failures, tally)
check_temporary(claude_md, failures, tally)
check_rules(root, failures, tally)
check_workspace_copy(os.path.dirname(root), failures, tally)
check_memory(os.path.dirname(root), failures, warnings, tally)
check_citations(root, 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))
for f in failures:
print(" - %s" % f)
return 1
print("")
print("instructions_gate: OK%s" % (" (%d warning(s))" % len(warnings) if warnings else ""))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))