One ! line disables the whole .neo-ai-data npm exclusion — logs and the Memory Core graph become packable
Context
Found while verifying #17240's package measurements. npm pack --dry-run --json in a working checkout with live Agent OS state lists, among the ten largest files in the tarball:
0.69 MiB .neo-ai-data/sqlite/memory-core-graph.sqlite
and 22 further entries under .neo-ai-data/: every kb-server / mc-server / nl-server daily log, deployment-state/snapshot.json, harness-state/*.json, and wake-daemon/ in-flight files.
No leak has occurred, and that is stated first because the rest of this ticket reads worse than the situation is. The published neo.mjs@13.1.0 tarball was fetched from the registry and inspected: it contains exactly two .neo-ai-data entries, concepts/edges.jsonl and concepts/nodes.jsonl — the intended carve-out, nothing else. The published artifact is clean.
It is clean by environment, not by rule. The release happened to be cut from a checkout whose .neo-ai-data held only concepts/. The rule that was supposed to guarantee it does not.
The Problem
.npmignore:108-110:
.env
.neo-ai-data
!.neo-ai-data/concepts/
The intent is obvious and reasonable: exclude the Agent OS plane data, re-include the concept graph the package legitimately ships. The second half silently destroys the first.
Isolated in a controlled two-case repro — a scratch package containing concepts/nodes.jsonl, logs/server.log, and sqlite/graph.sqlite, packed twice with only .npmignore differing:
.npmignore |
npm pack --dry-run result |
.neo-ai-data + !.neo-ai-data/concepts/ |
concepts/nodes.jsonl, logs/server.log, sqlite/graph.sqlite |
.neo-ai-data alone |
nothing under .neo-ai-data |
The negation forces ignore-walk to descend into the excluded directory, and once it descends, the bare .neo-ai-data pattern no longer suppresses the siblings it was written to suppress. The carve-out does not widen the exclusion by one directory — it removes it.
This is worse than an exclusion that never matched (#17240's stale path). That one fails visibly the moment anyone measures. This one produces a correct-looking package on any machine where the directory happens to hold only the carved-out subtree, and an incorrect one everywhere else — so measuring it once, on the wrong machine, confirms the wrong answer.
Why it matters beyond size
The three largest packable items under that directory are not bulk:
sqlite/memory-core-graph.sqlite — the Memory Core graph: agent memories, session records, A2A message edges.
logs/{kb,mc,nl}-server-*.log — server logs, which are not written under any redaction contract for this purpose.
deployment-state/snapshot.json, harness-state/*.json, wake-daemon/* — resolved deployment topology and per-seat harness state.
None of it has a consumer inside an installed package. All of it describes the maintainer's machine and the swarm's private working state.
The Architectural Reality
package.json declares no files array, so .npmignore is the sole gate on package contents — every line is load-bearing and, as #17240 established for a different line, unverified.
.gitignore:114 also lists .neo-ai-data, which is why this never shows up in git status and reads as safe. npm ignores .gitignore entirely when an .npmignore exists, so the git-side protection contributes nothing here.
- Nothing in CI or the release path asserts tarball contents.
buildScripts/release/publish.mjs cuts the release commit; package composition is never checked against an expectation.
The Fix
- Remove the negation and exclude
.neo-ai-data outright, then re-include the concepts subtree by a mechanism that does not re-open the directory — the reliable form is to not exclude the parent as a bare directory at all, but to exclude the specific sibling subtrees, or to stop shipping concepts/ from that path. Whichever shape is chosen, it must be demonstrated by npm pack --dry-run, not argued from pattern semantics — reasoning about ignore-file semantics is precisely what produced this defect.
- Decide explicitly whether
.neo-ai-data/concepts/ should ship at all. It is currently the only reason the negation exists. If the package needs that data, a path outside the plane directory is a better home for it than a carve-out inside an exclusion that guards private state.
- Add a packaging assertion so this class cannot regress silently: a check that fails when the tarball contains anything under
.neo-ai-data/ other than the explicitly allowed set. The defect class here is not "one wrong pattern" but "no observation of what actually ships".
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
| published tarball contents |
.npmignore (no files array) |
zero .neo-ai-data entries beyond an explicit allowlist |
none — exclusion is total |
.npmignore comments |
npm pack --dry-run --json before/after |
.npmignore:109-110 |
this ticket |
negation removed or restructured |
n/a |
inline comment naming the descend behavior |
the two-case repro above |
| packaging assertion |
new |
fails the build on an unexpected .neo-ai-data entry |
n/a |
script JSDoc |
red-proof required |
Decision Record impact
none.
Acceptance Criteria
Out of Scope
- Migrating
package.json to a files allowlist. Strictly the better long-term shape, and #17240 already defers it for the same reason: it is a large behavior change that deserves its own ticket rather than riding a fix.
- The DevIndex corpus exclusion itself (#17240) — same file, different defect (a stale path rather than a defeated exclusion). Expected to be fixed in the same PR because two concurrent edits to
.npmignore would conflict, but they are separate close targets.
- Auditing the remaining
.npmignore entries one by one. The packaging assertion in AC-3 is the general answer; an entry-by-entry audit is the thing the assertion exists to make unnecessary.
Avoided Traps
- Reporting this as a live leak. The registry artifact was fetched and checked before writing anything. It is clean. Filing "we shipped the memory graph" would have been false, and the correction would have cost more than the ticket.
- Fixing it by pattern reasoning. The original line is what careful pattern reasoning produces. Only
npm pack --dry-run against a checkout that actually holds the files can settle it.
- Treating it as a size problem. It is adjacent to #17240 and will share a PR, but 0.69 MiB is not why this matters.
Related
- #17240 — the sibling
.npmignore defect (stale path); same file, expected same PR, separate close target
- #17238 — the git-history side of the same corpus question
.npmignore, buildScripts/release/publish.mjs
Origin Session ID: 3f264a19-c7d4-481e-bc80-5c288bca177f
Retrieval Hint: query_raw_memories("npmignore negation defeats directory exclusion neo-ai-data packable")
Live latest-open sweep: checked latest 20 open issues at 2026-08-16T20:10Z; A2A claim sweep over the last 8 messages at the same time; no equivalent ticket and no competing lane claim found.
One
!line disables the whole.neo-ai-datanpm exclusion — logs and the Memory Core graph become packableContext
Found while verifying #17240's package measurements.
npm pack --dry-run --jsonin a working checkout with live Agent OS state lists, among the ten largest files in the tarball:and 22 further entries under
.neo-ai-data/: everykb-server/mc-server/nl-serverdaily log,deployment-state/snapshot.json,harness-state/*.json, andwake-daemon/in-flight files.No leak has occurred, and that is stated first because the rest of this ticket reads worse than the situation is. The published
neo.mjs@13.1.0tarball was fetched from the registry and inspected: it contains exactly two.neo-ai-dataentries,concepts/edges.jsonlandconcepts/nodes.jsonl— the intended carve-out, nothing else. The published artifact is clean.It is clean by environment, not by rule. The release happened to be cut from a checkout whose
.neo-ai-dataheld onlyconcepts/. The rule that was supposed to guarantee it does not.The Problem
.npmignore:108-110:The intent is obvious and reasonable: exclude the Agent OS plane data, re-include the concept graph the package legitimately ships. The second half silently destroys the first.
Isolated in a controlled two-case repro — a scratch package containing
concepts/nodes.jsonl,logs/server.log, andsqlite/graph.sqlite, packed twice with only.npmignorediffering:.npmignorenpm pack --dry-runresult.neo-ai-data+!.neo-ai-data/concepts/concepts/nodes.jsonl,logs/server.log,sqlite/graph.sqlite.neo-ai-dataalone.neo-ai-dataThe negation forces
ignore-walkto descend into the excluded directory, and once it descends, the bare.neo-ai-datapattern no longer suppresses the siblings it was written to suppress. The carve-out does not widen the exclusion by one directory — it removes it.This is worse than an exclusion that never matched (#17240's stale path). That one fails visibly the moment anyone measures. This one produces a correct-looking package on any machine where the directory happens to hold only the carved-out subtree, and an incorrect one everywhere else — so measuring it once, on the wrong machine, confirms the wrong answer.
Why it matters beyond size
The three largest packable items under that directory are not bulk:
sqlite/memory-core-graph.sqlite— the Memory Core graph: agent memories, session records, A2A message edges.logs/{kb,mc,nl}-server-*.log— server logs, which are not written under any redaction contract for this purpose.deployment-state/snapshot.json,harness-state/*.json,wake-daemon/*— resolved deployment topology and per-seat harness state.None of it has a consumer inside an installed package. All of it describes the maintainer's machine and the swarm's private working state.
The Architectural Reality
package.jsondeclares nofilesarray, so.npmignoreis the sole gate on package contents — every line is load-bearing and, as #17240 established for a different line, unverified..gitignore:114also lists.neo-ai-data, which is why this never shows up in git status and reads as safe. npm ignores.gitignoreentirely when an.npmignoreexists, so the git-side protection contributes nothing here.buildScripts/release/publish.mjscuts the release commit; package composition is never checked against an expectation.The Fix
.neo-ai-dataoutright, then re-include the concepts subtree by a mechanism that does not re-open the directory — the reliable form is to not exclude the parent as a bare directory at all, but to exclude the specific sibling subtrees, or to stop shippingconcepts/from that path. Whichever shape is chosen, it must be demonstrated bynpm pack --dry-run, not argued from pattern semantics — reasoning about ignore-file semantics is precisely what produced this defect..neo-ai-data/concepts/should ship at all. It is currently the only reason the negation exists. If the package needs that data, a path outside the plane directory is a better home for it than a carve-out inside an exclusion that guards private state..neo-ai-data/other than the explicitly allowed set. The defect class here is not "one wrong pattern" but "no observation of what actually ships".Contract Ledger Matrix
.npmignore(nofilesarray).neo-ai-dataentries beyond an explicit allowlist.npmignorecommentsnpm pack --dry-run --jsonbefore/after.npmignore:109-110.neo-ai-dataentryDecision Record impact
none.Acceptance Criteria
npm pack --dry-runin a checkout containing live.neo-ai-data/logs/,.neo-ai-data/sqlite/and.neo-ai-data/wake-daemon/lists zero entries from those directories. The checkout must actually contain them — a clean checkout cannot falsify this, and that is the entire lesson of the ticket..neo-ai-data/concepts/is recorded explicitly: still shipped (and from where), or dropped..neo-ai-dataentry is present, red-proofed by temporarily adding one.npm pack --dry-run --jsonoutput, not from arithmetic.neo.mjs@13.1.0was inspected and is clean, so this is a guard repair and not an incident response.Out of Scope
package.jsonto afilesallowlist. Strictly the better long-term shape, and #17240 already defers it for the same reason: it is a large behavior change that deserves its own ticket rather than riding a fix..npmignorewould conflict, but they are separate close targets..npmignoreentries one by one. The packaging assertion in AC-3 is the general answer; an entry-by-entry audit is the thing the assertion exists to make unnecessary.Avoided Traps
npm pack --dry-runagainst a checkout that actually holds the files can settle it.Related
.npmignoredefect (stale path); same file, expected same PR, separate close target.npmignore,buildScripts/release/publish.mjsOrigin Session ID: 3f264a19-c7d4-481e-bc80-5c288bca177f
Retrieval Hint:
query_raw_memories("npmignore negation defeats directory exclusion neo-ai-data packable")Live latest-open sweep: checked latest 20 open issues at 2026-08-16T20:10Z; A2A claim sweep over the last 8 messages at the same time; no equivalent ticket and no competing lane claim found.