Filed unassigned by the domain:devx seat while measuring #11094 (session session_01UjM2ia8Av1v5NqfqQEQmC6). Recording only — not graded here, and deliberately not fixed under #11094, whose dispatch rules packages/cli/src/commands/dev.ts out of scope as another lane's surface.
⛔ This does not re-open #11020. That card's fix is present and correct on main; this is an observation about the shape of the guard around it, not a claim that the fix is wrong.
The mechanism
PR #11143 removed the explicit NODE_ENV: 'development' write from dev.ts's compile spawn. The spawn that replaced it is:
const compileResult = spawnSync(
process.execPath,
[binPath, 'compile', '--output', artifactPath],
{ stdio: 'inherit' },
);
No env option, so the child inherits process.env whole. The fix removed the write; it did not scrub the variable. So the child is under oclif's tsx source loader whenever the parent already is — for example under tsx packages/cli/bin/run-dev.js dev, since bin/run-dev.js:28 does process.env.NODE_ENV = 'development' (deliberately — it inlines oclif's development: true), or for any developer with NODE_ENV=development exported.
Why the existing pin cannot see it
packages/cli/src/commands/child-env-source-loader.pin.test.ts is green (4 tests) and its property is exactly right for what it asserts: no command source writes a loader-activating NODE_ENV into a child environment. Two structural consequences:
- It reads writes off the TypeScript AST. Inheritance is the absence of a write, so it is invisible to the detector by construction — not a bug in the pin, a boundary of its property.
- Its walk is
COMMANDS_DIR (packages/cli/src/commands/, .ts only, recursive). packages/cli/bin/run-dev.js — the one file in the repo that still deliberately arms the loader — is outside it on both counts (different directory, .js).
Note the pin's own exemption rule would exempt run-dev.js anyway even if it were in the walk: a process.env write in a file that starts no child process is treated as a command setting its own mode. That is correct in isolation; it just means nothing connects "this process arms the loader" to "this process later spawns children that inherit it".
Measured, on origin/main @ 2c4c59ed20
The hazard is currently latent, not live — the pair can be fully armed and does not detonate:
| cwd |
sibling-src paths entries |
NODE_ENV=development … run-dev.js compile |
examples/app-showcase |
2 |
exit 0 |
examples/app-crm |
1 |
exit 0 |
examples/app-todo |
0 |
exit 0 |
The redirect itself still fires — simulating oclif's exact registration (require('tsx/cjs/api').register(), per @oclif/core@4.13.3/lib/config/ts-path.js) from an app-showcase cwd resolves @objectstack/formula to /packages/formula/src/index.ts and requires it successfully. tsx's CJS hook resolves the extensionless sibling ./registry that the original report recorded as failing.
⚠️ But it is conditioned on build state. Before the dependency closure was built, the same redirect failed through the same path:
Error: Cannot find module '.../packages/formula/node_modules/@objectstack/spec/dist/index.js'
at nextResolveSimple (.../tsx@4.23.12/dist/register-C557imBs.cjs:10:1006)
So redirecting a package to its source makes runtime resolution depend on that package's own dependency dist/s existing — which a static gate reading tsconfigs cannot observe, and which varies with what the developer has built.
Why it is worth recording rather than fixing now
Possible shapes (not chosen here)
- A — pin that no spawn of
bin/run-dev.js uses a cwd inside a package whose tsconfig carries a sibling-src paths entry. Measures the real composition; needs no resolver simulation.
- B — scrub
NODE_ENV explicitly on the compile spawn (env: { ...process.env, NODE_ENV: undefined }) so the property holds regardless of the ambient environment, and widen the pin to assert the scrub rather than the absence of a write.
- C — leave it recorded. The comment block at
dev.ts:192 already explains the hazard at length; this issue is the note that the enforcement is narrower than the comment.
⚠️ #8020 / #8108 are the standing precedent against anything that simulates resolution: both were defects in a gate that read a correct config as wrong by failing to parse one spelling of it. A and B avoid that class; a general paths-resolution gate would inherit it.
Searched before filing
os dev compile child inherits ambient NODE_ENV development oclif source loader → 3 hits, none duplicate: #11773 (open — childEnv() forwarding NODE_PATH, a different variable and consequence), #11317 (closed — e2e tests spawning bin/run.js reaching src/ instead of dist/ under the same loader), #5673 (closed — unrelated /discovery broadcast). ⚠️ Channel note: this search used the MCP search_issues endpoint rather than the REST list endpoint, because direct REST from this session returns "GitHub access is not enabled for this session".
Related: #11094 (the card this was measured under) · #11020 / PR #11143 (the fix) · #8249 (the growing paths population) · #8180 · #7849.
Generated by Claude Code
Filed unassigned by the
domain:devxseat while measuring #11094 (sessionsession_01UjM2ia8Av1v5NqfqQEQmC6). Recording only — not graded here, and deliberately not fixed under #11094, whose dispatch rulespackages/cli/src/commands/dev.tsout of scope as another lane's surface.⛔ This does not re-open #11020. That card's fix is present and correct on
main; this is an observation about the shape of the guard around it, not a claim that the fix is wrong.The mechanism
PR #11143 removed the explicit
NODE_ENV: 'development'write fromdev.ts's compile spawn. The spawn that replaced it is:No
envoption, so the child inheritsprocess.envwhole. The fix removed the write; it did not scrub the variable. So the child is under oclif's tsx source loader whenever the parent already is — for example undertsx packages/cli/bin/run-dev.js dev, sincebin/run-dev.js:28doesprocess.env.NODE_ENV = 'development'(deliberately — it inlines oclif'sdevelopment: true), or for any developer withNODE_ENV=developmentexported.Why the existing pin cannot see it
packages/cli/src/commands/child-env-source-loader.pin.test.tsis green (4 tests) and its property is exactly right for what it asserts: no command source writes a loader-activatingNODE_ENVinto a child environment. Two structural consequences:COMMANDS_DIR(packages/cli/src/commands/,.tsonly, recursive).packages/cli/bin/run-dev.js— the one file in the repo that still deliberately arms the loader — is outside it on both counts (different directory,.js).Note the pin's own exemption rule would exempt
run-dev.jsanyway even if it were in the walk: aprocess.envwrite in a file that starts no child process is treated as a command setting its own mode. That is correct in isolation; it just means nothing connects "this process arms the loader" to "this process later spawns children that inherit it".Measured, on
origin/main@2c4c59ed20The hazard is currently latent, not live — the pair can be fully armed and does not detonate:
pathsentriesNODE_ENV=development … run-dev.js compileexamples/app-showcaseexamples/app-crmexamples/app-todoThe redirect itself still fires — simulating oclif's exact registration (
require('tsx/cjs/api').register(), per@oclif/core@4.13.3/lib/config/ts-path.js) from anapp-showcasecwd resolves@objectstack/formulato/packages/formula/src/index.tsand requires it successfully.tsx's CJS hook resolves the extensionless sibling./registrythat the original report recorded as failing.So redirecting a package to its source makes runtime resolution depend on that package's own dependency
dist/s existing — which a static gate reading tsconfigs cannot observe, and which varies with what the developer has built.Why it is worth recording rather than fixing now
tsx/run-dev.jsspawn in the repo uses amkdtempcwd or apaths-free one, and both CI paths that run the CLI against example apps (ci.yml'sverifyloop,showcase-smoke.yml's playwrightwebServer) use the publishedbin/run.js, which writes noNODE_ENV.paths-carrying package. Nothing currently refuses that.pathsentries in the tree today. The first half of the pair is set to grow ~26x.Possible shapes (not chosen here)
bin/run-dev.jsuses a cwd inside a package whose tsconfig carries a sibling-srcpathsentry. Measures the real composition; needs no resolver simulation.NODE_ENVexplicitly on the compile spawn (env: { ...process.env, NODE_ENV: undefined }) so the property holds regardless of the ambient environment, and widen the pin to assert the scrub rather than the absence of a write.dev.ts:192already explains the hazard at length; this issue is the note that the enforcement is narrower than the comment.paths-resolution gate would inherit it.Searched before filing
os dev compile child inherits ambient NODE_ENV development oclif source loader→ 3 hits, none duplicate: #11773 (open —childEnv()forwardingNODE_PATH, a different variable and consequence), #11317 (closed — e2e tests spawningbin/run.jsreachingsrc/instead ofdist/under the same loader), #5673 (closed — unrelated/discoverybroadcast).search_issuesendpoint rather than the REST list endpoint, because direct REST from this session returns "GitHub access is not enabled for this session".Related: #11094 (the card this was measured under) · #11020 / PR #11143 (the fix) · #8249 (the growing
pathspopulation) · #8180 · #7849.Generated by Claude Code