Context
Found while dry-running the v13.2 deploy for #19047: the pages pipeline installs the engine as an npm dependency, so I installed engine dev the same way. It fails before any build step runs.
Reproduced 2026-09-22 on dev@e66b6f8142:
git worktree add --detach <wt> origin/dev
cd <wt> && npm pack --ignore-scripts # neo.mjs-13.1.0.tgz, 5,714 files
mkdir <consumer> && cd <consumer>
echo '{"name":"c","version":"1.0.0","private":true}' > package.json
npm install <wt>/neo.mjs-13.1.0.tgz
→ npm error code 127
→ npm error command sh -c neo-agent-skills-materialize
→ npm error sh: neo-agent-skills-materialize: command not foundThe Problem
package.json declares "postinstall": "neo-agent-skills-materialize", and neo-agent-skills is a devDependency. npm runs a dependency's postinstall inside every consumer's install, and a consumer never installs its dependencies' devDependencies. So the binary exists where the hook runs only if the consumer's own tree supplies it.
No published version carries it yet. neo.mjs@13.1.0 (npm, 2026-07-03) has no postinstall; the hook arrived on dev on 2026-08-26 via #17799. Measured 2026-09-22 with packed tarballs (npm 11):
| Consumer shape |
dev |
this fix |
empty project, npm install <tgz> |
exit 127 |
exit 0 |
create-app scaffold as generated today (postinstall: cd node_modules/neo.mjs && npm i) — the same order as pages' step 4 (install) → step 7 (npm i on neo) |
exit 127: the outer install fires the hook before the consumer's own npm i on neo runs |
exit 0; the nested npm i runs prepare and materializes 37 links inside node_modules/neo.mjs |
a workspace that declares neo-agent-skills itself, with no hook of its own |
exit 0, and 37 links projected at the workspace root — the dependency script finds the consumer's .bin, and INIT_CWD is the consumer root |
exit 0, 0 links — the workspace runs the materializer itself |
So 13.2.0 would break every default-scripts install whose tree lacks neo-agent-skills: pages (step 4 dies before step 7), devindex (declares no skills), today's create-app scaffold, and third-party projects. (pnpm and bun block dependency lifecycle scripts by default.)
The last row is a behaviour this fix removes, not only a failure it cures: on dev, a consumer that happens to carry the skills gets them projected into its root by the engine. Fork resolved: consumer-owned projection. The alternative — an engine postinstall script that runs the materializer only when neo-agent-skills resolves from the installing tree — was put to @neo-gpt at @tobiu's suggestion, and rejected: that root projection is an incidental cross-package effect of the engine's hook, not a promise #17798 makes to consumers (it gives each consumer its own dependency and materializer), and keeping it would preserve an engine-controlled write into a consumer's tree. Corrected 2026-09-22: the first version of this section claimed every default-scripts install would fail.
The Architectural Reality
#17798 chose install-time materialization so each repository that uses the skills gets projected links in its own checkout. That intent is right. What it missed is that neo.mjs is also a published package, so its own install hook fires in other people's installs.
- npm already separates the two contexts: a registry or tarball install never runs
prepare, while a dependency's postinstall runs in every consumer's install. prepare does run on an argument-less npm install / npm ci in the package's own directory, on pack/publish, and for Git-dependency and linked-folder installs, which build from a checkout (npm v11 scripts); neither of the last two is a supported way to consume neo.mjs, and neither was measured here. The engine's own-checkout lifecycle already lives in prepare: buildScripts/util/prepare.mjs (runPrepare), which short-circuits on --package-lock-only and resolves husky from its manifest's bin so it works under cmd.exe.
- The materializer finds its target through
INIT_CWD (materialize-harness-skills.mjs consumerRoot()), which npm sets identically for prepare, so the projection lands in the same place.
neo-agent-brain is unaffected on its own: it is private, and neo-agent-skills is a regular dependency there.
The Fix
package.json — delete scripts.postinstall.
buildScripts/util/prepare.mjs — generalize resolveHuskyBin into a package-and-bin resolver, and give runPrepare a second stage after husky: neo-agent-skills-materialize, resolved from node_modules/neo-agent-skills/package.json bin, launched through process.execPath. The lock-only guard still returns before either stage; a materializer failure fails the install, as husky's does.
test/playwright/unit/buildScripts/prepare.spec.mjs — "husky is the only Engine prepare step" becomes husky-then-materializer; add the failure, missing-package and missing-bin arms.
- Comments that name
postinstall as the materialization path (.gitignore, .github/workflows/substrate-sync.yml, .github/workflows/guard-ci-parity-lint.yml) say prepare.
- Consequence for consumers: one that wants skills runs
neo-agent-skills-materialize from its own lifecycle — #17798's consumer contract, which the Brain already follows. For create-app, that line travels with the devDependency sync in neomjs/create-app#34.
Contract Ledger
| Target surface |
Source of authority |
Proposed behavior |
Fallback |
Docs |
Evidence |
package.json scripts.postinstall |
#17798 mechanism |
removed |
— |
prepare.mjs docblock |
consumer probe above: exit 127 |
buildScripts/util/prepare.mjs runPrepare |
the engine's own-checkout lifecycle |
lock-only guard → husky → materializer; either failure fails the install |
npm ci --ignore-scripts skips it, as before — substrate-sync.yml's --check stays the arm |
docblock |
prepare.spec.mjs |
a registry or tarball install of neo.mjs |
npm lifecycle semantics |
runs no engine script |
— |
— |
the same probe, exit 0 |
Decision Record impact
amends #17798 AC-2, narrowly and for the Engine only. That AC required neomjs/neo, the Brain and devindex each to run the materializer from postinstall; the Engine moves it to prepare because it is also a published package, and the consumer-install falsifier above is why. Everything else #17798 settled stands: the consumer-owned dependency and projection, the Brain's and devindex's own hooks, and the --check arm. No ADR is involved. (@neo-gpt's source-of-authority read, R1 preflight on PR #19053.)
Acceptance Criteria
Out of Scope
- Whether the published tarball should carry materialized links at all (
npm publish runs after npm ci, with or without this change) — that belongs to check-package-contents.yml's gate.
- The pages topology —
D#19050.
Avoided Traps
- Moving
neo-agent-skills into dependencies. Consumer installs pass, but every library user downloads agent substrate and runs the materializer against their own project root via INIT_CWD.
- A shell guard (
command -v … || true). POSIX-only — prepare.mjs exists because cmd.exe has no [ — and it would swallow a real materializer failure in the engine's own checkout.
- Detecting "am I a dependency?" in a script. Re-implements what npm's
prepare/postinstall split already encodes.
Related
- #19047 — its AC-4 dry run installs the engine as a dependency, so it cannot pass until this lands
#17798 — origin of the hook (closed)
Sweeps
- Live latest-open sweep: latest 20 open issues at 2026-09-22T22:12:26Z — no equivalent.
- A2A in-flight sweep: last 30 messages, all read-states, at 22:12Z — no claim on this scope.
- MC sweep:
query_raw_memories("npm install neo.mjs fails postinstall neo-agent-skills-materialize command not found consumer workspace devDependency"), 6 results, no prior decision found.
- Own-assignment sweep: 8 open, none overlapping (
#18985's contributor npm install runs in the engine's own checkout, which this keeps working).
- Mirror grep:
resources/content/issues for neo-agent-skills-materialize → only #17798.
Origin Session ID: 3f07edfa-63cf-4d5d-9c78-1e0d592ce98f
Retrieval Hint: query_raw_memories("neo.mjs postinstall devDependency consumer install exit 127 prepare lifecycle")
Authored by ⚖️ Ada · @neo-opus-ada · Claude Opus 5.5 · Claude Code
Context
Found while dry-running the v13.2 deploy for #19047: the pages pipeline installs the engine as an npm dependency, so I installed engine
devthe same way. It fails before any build step runs.Reproduced 2026-09-22 on
dev@e66b6f8142:git worktree add --detach <wt> origin/dev cd <wt> && npm pack --ignore-scripts # neo.mjs-13.1.0.tgz, 5,714 files mkdir <consumer> && cd <consumer> echo '{"name":"c","version":"1.0.0","private":true}' > package.json npm install <wt>/neo.mjs-13.1.0.tgz → npm error code 127 → npm error command sh -c neo-agent-skills-materialize → npm error sh: neo-agent-skills-materialize: command not foundThe Problem
package.jsondeclares"postinstall": "neo-agent-skills-materialize", andneo-agent-skillsis a devDependency. npm runs a dependency'spostinstallinside every consumer's install, and a consumer never installs its dependencies' devDependencies. So the binary exists where the hook runs only if the consumer's own tree supplies it.No published version carries it yet.
neo.mjs@13.1.0(npm, 2026-07-03) has nopostinstall; the hook arrived ondevon 2026-08-26 via#17799. Measured 2026-09-22 with packed tarballs (npm 11):devnpm install <tgz>create-appscaffold as generated today (postinstall: cd node_modules/neo.mjs && npm i) — the same order as pages' step 4 (install) → step 7 (npm ion neo)npm ion neo runsnpm irunsprepareand materializes 37 links insidenode_modules/neo.mjsneo-agent-skillsitself, with no hook of its own.bin, andINIT_CWDis the consumer rootSo 13.2.0 would break every default-scripts install whose tree lacks
neo-agent-skills: pages (step 4 dies before step 7), devindex (declares no skills), today'screate-appscaffold, and third-party projects. (pnpm and bun block dependency lifecycle scripts by default.)The last row is a behaviour this fix removes, not only a failure it cures: on
dev, a consumer that happens to carry the skills gets them projected into its root by the engine. Fork resolved: consumer-owned projection. The alternative — an enginepostinstallscript that runs the materializer only whenneo-agent-skillsresolves from the installing tree — was put to @neo-gpt at @tobiu's suggestion, and rejected: that root projection is an incidental cross-package effect of the engine's hook, not a promise#17798makes to consumers (it gives each consumer its own dependency and materializer), and keeping it would preserve an engine-controlled write into a consumer's tree. Corrected 2026-09-22: the first version of this section claimed every default-scripts install would fail.The Architectural Reality
#17798chose install-time materialization so each repository that uses the skills gets projected links in its own checkout. That intent is right. What it missed is thatneo.mjsis also a published package, so its own install hook fires in other people's installs.prepare, while a dependency'spostinstallruns in every consumer's install.preparedoes run on an argument-lessnpm install/npm ciin the package's own directory, on pack/publish, and for Git-dependency and linked-folder installs, which build from a checkout (npm v11 scripts); neither of the last two is a supported way to consumeneo.mjs, and neither was measured here. The engine's own-checkout lifecycle already lives inprepare:buildScripts/util/prepare.mjs(runPrepare), which short-circuits on--package-lock-onlyand resolves husky from its manifest'sbinso it works undercmd.exe.INIT_CWD(materialize-harness-skills.mjsconsumerRoot()), which npm sets identically forprepare, so the projection lands in the same place.neo-agent-brainis unaffected on its own: it isprivate, andneo-agent-skillsis a regular dependency there.The Fix
package.json— deletescripts.postinstall.buildScripts/util/prepare.mjs— generalizeresolveHuskyBininto a package-and-bin resolver, and giverunPreparea second stage after husky:neo-agent-skills-materialize, resolved fromnode_modules/neo-agent-skills/package.jsonbin, launched throughprocess.execPath. The lock-only guard still returns before either stage; a materializer failure fails the install, as husky's does.test/playwright/unit/buildScripts/prepare.spec.mjs— "husky is the only Engine prepare step" becomes husky-then-materializer; add the failure, missing-package and missing-bin arms.postinstallas the materialization path (.gitignore,.github/workflows/substrate-sync.yml,.github/workflows/guard-ci-parity-lint.yml) sayprepare.neo-agent-skills-materializefrom its own lifecycle —#17798's consumer contract, which the Brain already follows. Forcreate-app, that line travels with the devDependency sync in neomjs/create-app#34.Contract Ledger
package.jsonscripts.postinstall#17798mechanismprepare.mjsdocblockbuildScripts/util/prepare.mjsrunPreparenpm ci --ignore-scriptsskips it, as before —substrate-sync.yml's--checkstays the armprepare.spec.mjsneo.mjsDecision Record impact
amends #17798 AC-2, narrowly and for the Engine only. That AC requiredneomjs/neo, the Brain and devindex each to run the materializer frompostinstall; the Engine moves it topreparebecause it is also a published package, and the consumer-install falsifier above is why. Everything else#17798settled stands: the consumer-owned dependency and projection, the Brain's and devindex's own hooks, and the--checkarm. No ADR is involved. (@neo-gpt's source-of-authority read, R1 preflight on PR #19053.)Acceptance Criteria
package.jsonhas nopostinstall;runPrepareruns the materializer after husky, resolved fromneo-agent-skills'sbindeclaration.devit exits 127 — red first, then green.npm cileavesnpx --no-install neo-agent-skills-materialize --checkgreen, andnpm install --package-lock-onlyruns neither stage.prepare.spec.mjscovers the stage order, a materializer failure failing the install, and a missing package or bin entry as a named error.Out of Scope
npm publishruns afternpm ci, with or without this change) — that belongs tocheck-package-contents.yml's gate.D#19050.Avoided Traps
neo-agent-skillsintodependencies. Consumer installs pass, but every library user downloads agent substrate and runs the materializer against their own project root viaINIT_CWD.command -v … || true). POSIX-only —prepare.mjsexists becausecmd.exehas no[— and it would swallow a real materializer failure in the engine's own checkout.prepare/postinstallsplit already encodes.Related
#17798— origin of the hook (closed)Sweeps
query_raw_memories("npm install neo.mjs fails postinstall neo-agent-skills-materialize command not found consumer workspace devDependency"), 6 results, no prior decision found.#18985's contributornpm installruns in the engine's own checkout, which this keeps working).resources/content/issuesforneo-agent-skills-materialize→ only#17798.Origin Session ID: 3f07edfa-63cf-4d5d-9c78-1e0d592ce98f
Retrieval Hint:
query_raw_memories("neo.mjs postinstall devDependency consumer install exit 127 prepare lifecycle")Authored by ⚖️ Ada ·
@neo-opus-ada· Claude Opus 5.5 · Claude Code