LearnNewsExamplesServices
Frontmatter
id17763
titleThe substrate guard measures the Claude load path, following both the symlink and @-import forms
stateClosed
labels
enhancementaitestingbuild
assigneesneo-opus-grace
createdAtAug 25, 2026, 5:22 PM
updatedAtAug 25, 2026, 5:51 PM
githubUrlhttps://github.com/neomjs/neo/issues/17763
authorneo-opus-grace
commentsCount0
parentIssue17175
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 25, 2026, 5:51 PM

The substrate guard measures the Claude load path, following both the symlink and @-import forms

Closed Backlog/active-chunk-19 enhancementaitestingbuild
neo-opus-grace
neo-opus-grace commented on Aug 25, 2026, 5:22 PM

Context

Delivery leaf for the unblocked half of #17175. That umbrella carries eight ACs; one of them (the @-import budgeting semantics experiment) is blocked on an authenticated seat and @neo-opus-vega correctly declined to solve it by entering credentials. Her closing line on the umbrella scopes this leaf exactly:

"Everything else on this ticket — the TARGET_FILES gap and the COMBINED_BUDGETS entry — is unblocked and independent of this result."

This leaf carries only the independent half, so a green delivery does not close a ticket whose blocked AC is untouched.

The Problem

ai/scripts/diagnostics/check-substrate-size.mjs watches AGENTS.md and .agents/ANTIGRAVITY_RULES.md. .claude/CLAUDE.md — what a Claude seat actually loads every turn — is in neither the target list nor the workflow's watch paths. A change to it therefore alters what every Claude seat loads while triggering nothing.

A stat-only check would not have helped either, because the entry point reaches its content two ways and the 2026-08-15 near-miss changed the file from one to the other:

form what a naive read reports what the seat loads
symlink (today) 12len('../AGENTS.md') via lstat 24,574
@-import file (proposed) 27 — the stub's own bytes via stat sum of its targets

So the guard must resolve both indirections or it computes the wrong total in whichever form it is not expecting.

Measured on dev at filing: AGENTS.md is 24,574 B against a 24,576 B limit. Two bytes. The guard passes and reports nothing about how close that is, which is the second half of the gap — this drift is gradual, so a shrinking margin is the signal, and by the time a verdict flips the substrate is already truncating.

The Architectural Reality

surface file role
per-file limit check-substrate-size.mjs (PER_FILE_LIMIT_BYTES) 24,576, documented in-repo as ANTIGRAVITY's constant
target list check-substrate-size.mjs (TARGET_FILES) the gap — Claude's entry point absent
group budgets check-substrate-size.mjs (COMBINED_BUDGETS) holds one entry, for a different surface
trigger .github/workflows/substrate-size-guard.yml .claude/** in neither pull_request nor push paths
Claude entry .claude/CLAUDE.md symlink → AGENTS.md (mode 120000)

A note on the number's substrate, per @neo-fable-clio's fold on the umbrella: 24,576 is ANTIGRAVITY's per-file limit. The Claude seat's own cap is an operator-stated constraint whose semantics — what it is measured over — are exactly what the blocked AC would settle. Inheriting one harness's constant into another's contract silently is how correct-looking arithmetic governs the wrong seat, so the Claude row must carry the assumption visibly rather than absorb it.

The Fix

Extend, do not build — the mechanism exists.

  1. Add .claude/CLAUDE.md to TARGET_FILES, and give entries a harness field so each row names who loads it and whether its limit is confirmed for that harness.
  2. Resolve the loaded size through both forms: realpathSync for symlinks (which also keys a cycle guard on file identity rather than the path used to reach it), and recursion through @-imports. Fail closed on a missing import target, matching the existing combined-budget branch.
  3. Add the Claude entry to both workflow watch-path lists.
  4. Report headroom on the per-file arm, as the combined arm already does.

Not a COMBINED_BUDGETS entry. The umbrella's fold suggested one for "the Claude load-path composition", and it does not fit today: per @neo-opus-vega's receipt the per-turn Claude composition is .claude/CLAUDE.md + MEMORY.md, and MEMORY.md is harness auto-memory living outside the repo entirely. A group budget would hold exactly one repo-resident member — a per-file limit wearing a group's clothes. If @-imports ever land in CLAUDE.md, the recursion in (2) is what expresses that composition, and it does so without a hardcoded member list.

Acceptance Criteria

  • check-substrate-size.mjs evaluates .claude/CLAUDE.md, resolving the symlink form to its target's bytes rather than the link's own.
  • The @-import form is measured as the sum of the importer plus its resolved targets, recursively, with a cycle guard and fail-closed handling of a missing target.
  • .github/workflows/substrate-size-guard.yml watch paths include the Claude entry point in both the pull_request and push lists.
  • The per-file arm reports remaining headroom alongside its verdict.
  • A red-then-green control reproduces the near-miss: .claude/CLAUDE.md importing AGENTS.md plus a 1,586 B second file fails, and the symlink-only tree passes. The failing arm carries a mutation control, so an arm that failed on any import is distinguishable from one that measures the sum.

Out of Scope

  • The @-import budgeting semantics experiment and whatever number it yields — blocked on an authenticated seat, stays on #17175.
  • turn-memory-pre-flight gaining a byte-delta output, and the prompt-submit-vs-boot-load documentation — separate surfaces, still on #17175.
  • Re-baselining PER_FILE_LIMIT_BYTES. Raising a limit to make a check pass inverts the point; the guard's own failure text says so.

Avoided Traps

  • Adding a second size lint. One exists, runs in CI, and has a group-budget mechanism. Building another would relocate the gap.
  • Trusting stat alone. It reports 12 for the symlink and 27 for an import stub. The instrument is the finding here — stat -f%z and fs.statSync disagree on this exact path, and only one of them is what the guard runs.
  • A group budget with one member. It would look like coverage while asserting nothing a per-file limit does not already assert.
  • Closing the umbrella. Four of its eight ACs land here; the blocked one is untouched, so Resolves belongs on this leaf and nowhere else.

Related

#17175 (umbrella — the blocked half stays there) · PR #17156 (the near-miss that rode green) · #15257 (the graduated budget the combined arm already holds)

Live latest-open sweep: checked latest 20 open issues at 2026-08-25T15:21:55Z, no equivalent found. A2A in-flight claim sweep over the last 30 messages at the same timestamp: no overlapping [lane-claim] on substrate-size or the Claude load path.

Structure gate: ai/scripts/diagnostics/ (40 files) is the owning folder; no new file is created, so the sibling-precedent fast-path applies.

Origin Session ID: 8daa7672-824e-4d4a-9283-8a0b908180c8

Retrieval Hint: query_raw_memories("substrate size guard Claude load path symlink @-import headroom")

🖖 Grace (@neo-opus-grace, Claude Opus 5, Claude Code)

tobiu closed this issue on Aug 25, 2026, 5:51 PM