Reshaped, 2026-09-25 15:15Z. The Fix and AC-2 changed; the first shape is kept below, struck. The operator challenged committing a region generated from .gitignore, and @neo-fable-clio's review amendment on #19223 proposed composing it at pack time. .npmignore is now composed by prepack and restored by postpack, so the committed file is its header alone. A git-dependency install runs no prepack (probe on npm 11.12.1). It packs the same files from the header anyway, because .gitignore matches no tracked file.
Correction, 2026-09-25 14:30Z. The premise changed; the original claim is kept below, struck. The 13.2 release would ship the Workstation. buildScripts/release/prepare.mjs rebuilds .npmignore's copy region from .gitignore, and .gitignore already carries the Workstation's three re-includes. Composed from dev, the release file adds 16 rules the committed copy lacks. The drop is real for every pack before the release step, because they read the committed file: CI's check-package-contents, an engine pinned by git commit (the Institution pins one), and #19047's dry run. That is the defect this ticket now covers.
Deployed from the published package, every one of those links would 404 on neomjs.com.
Context
#19047's AC-4 dry run, 2026-09-25: a scratch clone of neomjs/pages (62cb7a2f) with neo.mjs pinned to github:neomjs/neo#f9a776c50c, built by node buildScripts/updateNeoVersion.mjs --force --engine-ref=f9a776c50c… against the corpus pin 3894176e. The build exits 0 and was not pushed. Served and loaded headless, the portal, a learn route and the dashboard dock demos all mount with no console errors. The Workstation returns 404 in every mode (/apps/…, /dist/{development,production,esm}/…), because the installed package's apps/workstation/ holds only README.md. A git dependency installs through npm pack, which reads the committed .npmignore.
The Problem
.npmignore is an authoritative header plus a copy of .gitignore below # Original content of the .gitignore file. The release step regenerates that copy, but between releases it goes stale, and every pack reads the stale one:
- The package gate judges a different tarball.
check-package-contents runs npm pack --dry-run on the committed file, so its forbidden and required checks certify a file set the release will not publish. findRulesLostOnRelease covers one direction (a header rule a release would delete) and deliberately treats what the copy adds as the sync working.
- Git-pinned consumers get a different package. On
dev today that is the Workstation missing, among 16 drifted rules.
- A dry run cannot reproduce the release through the ordinary install path.
The Architectural Reality
buildScripts/util/npmIgnoreComposition.mjs#composeNpmIgnore is the single composition. The release step ran it; with this fix, prepack does.
- Drift measured on
dev (composeNpmIgnore(dev .npmignore, dev .gitignore) against the committed rules): +16 −1. The additions are the Workstation's three re-includes, resources/data/reports/, .neo-ai-secrets, .neo-ai-data-parity, /opencode.jsonc, /.kimi-code/{mcp.json,config.toml}, .agents/skills, .claude/skills/, .claude/hooks/* (plus its guard re-include), .codex/hooks/, and .kimi-code/hooks/* (plus its README). The removal is resources/content/sandman_handoff.md, which the header's resources/content/ subsumes.
- The copy only excludes untracked files.
git ls-files -ci --exclude-from=.gitignore is empty on dev, and a clean export packs 5,787 files with the copy and 5,787 without it. A working tree differs: there the header alone also ships gitignored local state.
- npm runs
prepack on npm pack and npm publish, never for a git dependency, which runs prepare. The publish job (npm-publish.yml) runs npm ci then npm publish, with scripts enabled.
The Fix
- The committed
.npmignore is its header, ending at the marker.
prepack composes the file in place (node ./buildScripts/util/npmIgnoreComposition.mjs compose), and postpack writes the header back (restore). findReleaseDrift and the release step's composition go. A pack that passes --ignore-scripts runs neither hook, so check-consumer-runtime-build composes around its pack (withComposedNpmIgnore).
checkPackageContents.spec.mjs arms: the committed file is its header alone; compose then restore is lossless; package.json wires both hooks; a fixture npm pack honours the composition, with and without the lifecycle; .gitignore matches no tracked file.
1. Commit the composition, so the committed file is what the release writes.
2. check-package-contents fails when they differ (findReleaseDrift: rules compared in order, because ignore files resolve last-match-wins) and prints the one-line sync command.
3. checkPackageContents.spec.mjs: arms for a missing rule, for order alone, for the in-sync case, and one reading the real files.
Acceptance Criteria
AC-2: check-package-contents fails when the committed .npmignore differs from its release composition. The arm reading the real files is red on dev (+16 −1) and green at the head.
Out of Scope
- The rest of #19047's AC-4 (the full dry run and its receipts).
- The portal home's links to unrendered learn pages (#19219).
Related
#19047 (parent) · #19219 · #19052 (the prepare / postinstall split the pages install depends on)
Live latest-open sweep: checked the latest 20 open issues at 2026-09-25T14:00:51Z, plus a search for existing Workstation or package-content reports; no equivalent.
A2A in-flight sweep: no claim on packaging or the Workstation's shipping in today's messages.
MC sweep: "published npm package omits an app because npmignore allowlist…", 5 results, no prior decision.
Own-assignment sweep: #19047 is the parent; nothing else overlaps.
Origin Session ID: 0f80515e-7682-4313-8101-b926da48c55c
Context
#19047's AC-4 dry run, 2026-09-25: a scratch clone of
neomjs/pages(62cb7a2f) withneo.mjspinned togithub:neomjs/neo#f9a776c50c, built bynode buildScripts/updateNeoVersion.mjs --force --engine-ref=f9a776c50c…against the corpus pin3894176e. The build exits 0 and was not pushed. Served and loaded headless, the portal, a learn route and the dashboard dock demos all mount with no console errors. The Workstation returns 404 in every mode (/apps/…,/dist/{development,production,esm}/…), because the installed package'sapps/workstation/holds onlyREADME.md. A git dependency installs throughnpm pack, which reads the committed.npmignore.The Problem
.npmignoreis an authoritative header plus a copy of.gitignorebelow# Original content of the .gitignore file. The release step regenerates that copy, but between releases it goes stale, and every pack reads the stale one:check-package-contentsrunsnpm pack --dry-runon the committed file, so its forbidden and required checks certify a file set the release will not publish.findRulesLostOnReleasecovers one direction (a header rule a release would delete) and deliberately treats what the copy adds as the sync working.devtoday that is the Workstation missing, among 16 drifted rules.The Architectural Reality
buildScripts/util/npmIgnoreComposition.mjs#composeNpmIgnoreis the single composition. The release step ran it; with this fix,prepackdoes.dev(composeNpmIgnore(dev .npmignore, dev .gitignore)against the committed rules): +16 −1. The additions are the Workstation's three re-includes,resources/data/reports/,.neo-ai-secrets,.neo-ai-data-parity,/opencode.jsonc,/.kimi-code/{mcp.json,config.toml},.agents/skills,.claude/skills/,.claude/hooks/*(plus its guard re-include),.codex/hooks/, and.kimi-code/hooks/*(plus its README). The removal isresources/content/sandman_handoff.md, which the header'sresources/content/subsumes.git ls-files -ci --exclude-from=.gitignoreis empty ondev, and a clean export packs 5,787 files with the copy and 5,787 without it. A working tree differs: there the header alone also ships gitignored local state.prepackonnpm packandnpm publish, never for a git dependency, which runsprepare. The publish job (npm-publish.yml) runsnpm cithennpm publish, with scripts enabled.The Fix
.npmignoreis its header, ending at the marker.prepackcomposes the file in place (node ./buildScripts/util/npmIgnoreComposition.mjs compose), andpostpackwrites the header back (restore).findReleaseDriftand the release step's composition go. A pack that passes--ignore-scriptsruns neither hook, socheck-consumer-runtime-buildcomposes around its pack (withComposedNpmIgnore).checkPackageContents.spec.mjsarms: the committed file is its header alone; compose then restore is lossless;package.jsonwires both hooks; a fixturenpm packhonours the composition, with and without the lifecycle;.gitignorematches no tracked file.1. Commit the composition, so the committed file is what the release writes.2.check-package-contentsfails when they differ (findReleaseDrift: rules compared in order, because ignore files resolve last-match-wins) and prints the one-line sync command.3.checkPackageContents.spec.mjs: arms for a missing rule, for order alone, for the in-sync case, and one reading the real files.Acceptance Criteria
npm pack --dry-runat the head lists all 28 trackedapps/workstation/files, with no forbidden entry and no missing required entry.npm packandnpm publishreads.npmignorecomposed from the committed header and the current.gitignore, and the committed file carries nothing below the marker. Spec arms pin both, plus the condition that lets a git-pinned install (noprepack) pack the same files:.gitignorematches no tracked file.devcommit that carries this, serves the four Workstation URLs with a mounted viewport. This is post-merge and recorded on #19047. Met 2026-09-25 15:45Z at2bad700dc0(receipt).AC-2:check-package-contentsfails when the committed.npmignorediffers from its release composition. The arm reading the real files is red ondev(+16 −1) and green at the head.Out of Scope
Related
#19047 (parent) · #19219 · #19052 (the
prepare/postinstallsplit the pages install depends on)Live latest-open sweep: checked the latest 20 open issues at 2026-09-25T14:00:51Z, plus a search for existing Workstation or package-content reports; no equivalent. A2A in-flight sweep: no claim on packaging or the Workstation's shipping in today's messages. MC sweep: "published npm package omits an app because npmignore allowlist…", 5 results, no prior decision. Own-assignment sweep: #19047 is the parent; nothing else overlaps.
Origin Session ID: 0f80515e-7682-4313-8101-b926da48c55c