LearnNewsExamplesServices
Frontmatter
id17697
titleA .gitignore negation git can never reach reads exactly like one that works
stateClosed
labels
bugtestingagent-os
assigneesneo-opus-vega
createdAtAug 24, 2026, 12:16 PM
updatedAtAug 24, 2026, 4:55 PM
githubUrlhttps://github.com/neomjs/neo/issues/17697
authorneo-opus-vega
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 24, 2026, 4:55 PM

A .gitignore negation git can never reach reads exactly like one that works

Closed Backlog/active-chunk-19 bugtestingagent-os
neo-opus-vega
neo-opus-vega commented on Aug 24, 2026, 12:16 PM

Context

Surfaced 2026-08-24 while writing the .gitignore for neomjs/neo-agent-brain (#17640). The operator directed me to compare it to the Engine's file. Copying the Agent OS block verbatim would have imported a rule that does nothing, so I probed it before carrying it across.

The Problem

.gitignore:111-112 reads:

.neo-ai-data
!.neo-ai-data/concepts/

Git does not descend into an ignored directory, so a negation below a bare directory rule is unreachable. Measured:

$ git check-ignore -v .neo-ai-data/concepts/probe.jsonl
.gitignore:111:.neo-ai-data     .neo-ai-data/concepts/probe.jsonl

The ignore rule wins; line 112 never participates.

Why nothing has broken yet, and why that is the dangerous part. .neo-ai-data/concepts/nodes.jsonl and edges.jsonl are tracked, so they are fine — git does not apply ignore rules to already-tracked paths. They survive because they predate the rule, not because the rule permits them. A third file added under concepts/ would be silently ignored: git add would report nothing, git status would show nothing, and the author would learn about it when a consumer read an incomplete corpus.

The repository already knows this trap. .gitignore:82-84 carries an inline comment on /docs/output/* saying exactly it — "Contents, NOT the directory: git does not descend into an ignored directory, so the negation below would be unreachable under a bare /docs/output" (#16600). The lesson was learned, written down, and the identical defect sits two blocks above it in the same file.

Census: 1 dead of 68

Every negation in .gitignore was probed. Exactly one is dead — the concepts one. The 20 !/apps/**/*.mjs-family lines and !.gemini/concepts/ are all reachable, because their parent rules are file globs or use the /* form.

The Architectural Reality

  • .gitignore:111 — the bare .neo-ai-data rule.
  • .gitignore:112 — the negation it masks.
  • .gitignore:82-84 — the same defect, already fixed, with the reason written in the file.
  • .gitignore:151-152 — .gemini/* + !.gemini/concepts/, the correct form, in this same file.
  • No check reads .gitignore. Verified by grep across buildScripts/ and ai/scripts/lint/. Its correctness rests entirely on whoever last edited it knowing this rule of git.

The instrument is part of the problem, and the ticket should say so. My first sweep reported 20 dead negations. git check-ignore exits 0 when any rule matches — including a negation — so every working ! line looked like it masked itself. The discriminator is the polarity of the matched pattern, not the exit code. Corrected with a positive control on a synthetic known-dead pair, the answer is 1. Any guard written for this must not repeat that error, which is why the AC below names the control rather than the count.

The Fix

  1. .neo-ai-data → .neo-ai-data/*, matching the /docs/output/* and .gemini/* forms already in the file. One character; the negation becomes reachable.
  2. A reachability guard, because a one-line fix regresses the moment someone adds the 69th negation. For each ! line, derive a probe path the negation intends to un-ignore, run git check-ignore -v, and fail when the winning pattern is an ignore rule rather than the negation itself.

The precedent shape is lintWorkflowScanRootParity.spec.mjs: a spec that guards a configuration invariant nothing else can see, running in the always-on unit lane rather than as a separate workflow.

Acceptance Criteria

  • .neo-ai-data/concepts/ is reachable: git check-ignore -v .neo-ai-data/concepts/<new-file> reports either no match or a match on the negation — never on an ignore rule.
  • Runtime plane data stays ignored — .neo-ai-data/graph.sqlite and a nested .neo-ai-data/sqlite/x.db both still match the ignore rule. The fix must not open the directory.
  • A guard fails when any negation in .gitignore is unreachable, deriving its verdict from the polarity of the matched pattern, never from git check-ignore's exit code.
  • POSITIVE CONTROL: the guard is proven against a synthetic known-dead pair (a bare directory plus a negation beneath it) and reports it dead. Without this the guard is indistinguishable from one that always passes — which is what my first census was.
  • NON-VACUITY: the guard is red on the current tree before the one-character fix lands, naming line 112.
  • The guard's reach is stated in its own output — that it checks .gitignore at the repository root and no other ignore file — so a green is never read as covering .git/info/exclude, a global ignore, or a nested .gitignore.
  • The tracked concepts files are unaffected: git ls-files .neo-ai-data/ still returns both, and the diff shows no content change under that path.

Out of Scope

  • Nested .gitignore files and .git/info/exclude. The guard names its reach rather than widening to surfaces whose population nobody has measured.
  • The 20 !/apps/**/*.mjs lines and the app whitelist generally. All reachable; a diff there would be a change without a defect.
  • neomjs/neo-agent-brain's copy. Already written in the correct form and verified there — this ticket is the Engine's own fix.
  • Whether .neo-ai-data/concepts/ should be tracked at all. It is, that is deliberate, and this ticket only makes the existing intent enforceable.

Avoided Traps

  • Filing "20 dead negations" from the first sweep. The count was an artifact of reading an exit code as a verdict. The corrected census is 1, and the wrong number is recorded above because the guard has to avoid the same mistake.
  • Fixing the line without the guard. The rule is subtle enough that the repository has now written it down once and violated it anyway. Discipline demonstrably did not hold.
  • Widening the guard to every ignore surface. An unmeasured population produces a green whose reach nobody can state.
  • Treating the surviving tracked files as evidence the rule works. They are evidence that git grandfathers tracked paths, which is a different fact and the reason this has stayed invisible.

Decision Record impact

none — repository hygiene with a mechanical guard; no ADR authority is touched.

Related

  • #17640 — the Brain scaffold, where the comparison surfaced this and where the correct form is already written.
  • #16600 — the /docs/output/* fix that learned this lesson for a different path.

Live latest-open sweep: latest 20 open issues checked 2026-08-24T10:14Z, plus an A2A claim scan over the last 30 messages and a grep across resources/content/issues/; no equivalent found. ai:structure-map gate: N/A on placement — this ticket makes no .mjs placement decision; the guard spec takes the test/playwright/unit/buildScripts/ sibling precedent. Correction: an earlier revision of this line also claimed the tool could not render the map, citing a peer defect-note. I ran it before relying on that a second time and it succeeds on this seat, exit 0 in both --files --loc and default modes. A sighting is not a standing fact, and I should not have passed one along as one.

Retrieval Hint: query_raw_memories("gitignore negation unreachable bare directory git does not descend check-ignore exit code"), or git check-ignore -v .neo-ai-data/concepts/probe.jsonl.

tobiu referenced in commit 046b88f - "fix(agentos): a negation git can never reach no longer reads like one that works (#17697) (#17698) on Aug 24, 2026, 4:55 PM
tobiu closed this issue on Aug 24, 2026, 4:55 PM