You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
⚠️ Definition of Done: this issue must be completed in full, in a single PR. Do not split this
work across multiple PRs, and do not defer any Deliverable below to a follow-up issue. A PR that
satisfies only some of the Deliverables, stubs a required test, or leaves a checkbox
partially-done does NOT resolve this issue and will be closed.
Context
packages/loopover-miner/lib/status.ts resolves the monorepo-sibling packages/loopover-engine
path relative to its own on-disk location, because this file runs from two different physical
directories depending on context: packages/loopover-miner/lib/status.ts (source, used by
in-process test imports) or packages/loopover-miner/dist/lib/status.js (the real compiled CLI,
since the 2026-07-24 out-of-place dist/ emit migration, commit c89679761, PR #8590).
That same commit introduced a depth-flexible helper specifically to handle this:
c89679761's commit message states: "Two production-source bugs found and fixed properly, not
patched around: bin/loopover-mcp.ts and lib/status.ts both compute paths relative to their own
file location ... Both now try the source-relative depth first, falling back to the dist-relative
depth, so they work correctly however they're currently loaded."
That fix was applied to the happy path of readInstalledEnginePackageVersion (line 180) and to
the pin-file argument of readExpectedEnginePackageVersion (line 223), but two sibling
call sites that compute the exact same monorepo-sibling path were left on the old
single-hardcoded-depth calculation, which is only correct from the source (lib/) directory, not
from the compiled (dist/lib/) one:
readInstalledEnginePackageVersion's catch-all fallback (lib/status.ts:184), reached when requireFromHere().resolve(ENGINE_PACKAGE) throws (Node cannot resolve @loopover/engine at
all):
// lib/status.ts, lines 176-193exportfunctionreadInstalledEnginePackageVersion(): string|null{try{returnreadInstalledEnginePackageVersionFromPaths(requireFromHere().resolve(ENGINE_PACKAGE),resolveMonorepoSiblingPath("..","loopover-engine","package.json"),// fixed);}catch{/* v8 ignore next 9 -- only reaches when Node cannot resolve the installed package at all */constworkspacePkg=join(moduleDir(),"../../loopover-engine/package.json");// NOT fixedif(existsSync(workspacePkg)){ ... }returnnull;}}
readExpectedEnginePackageVersion's first argument (monorepoEnginePkg, lib/status.ts:222):
// lib/status.ts, lines 220-224exportfunctionreadExpectedEnginePackageVersion(): string|null{returnreadExpectedEnginePackageVersionFromPaths(join(moduleDir(),"../../loopover-engine/package.json"),// NOT fixedresolveMonorepoSiblingPath("expected-engine.version"),// fixed);}
Empirically verified against the built package (npm --workspace @loopover/engine run build && npm --workspace @loopover/miner run build:tsc, then evaluating both path expressions for moduleDir() = packages/loopover-miner/lib vs packages/loopover-miner/dist/lib):
So when the real compiled CLI (dist/bin/loopover-miner.js, what doctor/status actually spawn
in production and in the CLI-harness e2e tests) runs inside a monorepo checkout:
readExpectedEnginePackageVersion()'s monorepoEnginePkg check for the live packages/loopover-engine/package.jsonalways silently fails (wrong path, existsSync
false), so it always falls through to the static expected-engine.version pin file instead
— even when a live, newer monorepo packages/loopover-engine/package.json is present. This
contradicts the function's own doc comment: "Expected minimum engine semver: monorepo engine
package.json when present, else the shipped pin file." The "when present" branch is dead code
from the compiled CLI's perspective.
readInstalledEnginePackageVersion()'s catch-all fallback (the last-resort path used when @loopover/engine cannot be resolved via require.resolve at all) computes the wrong monorepo
sibling path from dist/lib/, so it returns null instead of finding the real workspace
version — again, only from the compiled CLI's execution context.
This is a genuine regression introduced by c89679761 (verified: before that migration, source
and compiled output shared the same directory — packages/loopover-miner/lib/status.ts next to
its in-place-emitted status.js — so join(moduleDir(), "../../loopover-engine/package.json")
was correct from either execution mode; the dist/ split broke that assumption for these two
call sites only). Neither is covered by the existing test suite: test/unit/miner-status.test.ts only calls readInstalledEnginePackageVersionFromPaths / readExpectedEnginePackageVersionFromPaths directly with injected paths, or calls the
zero-arg wrapper functions from the .ts source context (where moduleDir() always resolves to lib/, never dist/lib/) — so the compiled-CLI code path these two spots are meant to serve is
completely unexercised, matching the /* v8 ignore next 9 */ annotation already sitting on the
catch block.
Requirements
Replace the two remaining hardcoded-depth path computations in packages/loopover-miner/lib/status.ts with the already-existing resolveMonorepoSiblingPath
helper, exactly the same way the other two call sites in the same file were already fixed:
readInstalledEnginePackageVersion's catch-all fallback (the workspacePkg local computed at
line 184) must use resolveMonorepoSiblingPath("..", "loopover-engine", "package.json")
instead of join(moduleDir(), "../../loopover-engine/package.json").
readExpectedEnginePackageVersion's first argument (line 222) must use resolveMonorepoSiblingPath("..", "loopover-engine", "package.json") instead of join(moduleDir(), "../../loopover-engine/package.json").
Do not change the behavior of either function for the currently-tested source (lib/)
execution context — both must continue to resolve the identical file they already resolve today
when moduleDir() is packages/loopover-miner/lib.
Add tests that exercise both fixed call sites from a simulated dist/lib/ directory depth
(i.e. construct a temporary directory tree with <tmp>/packages/loopover-miner/dist/lib/ as the
"module dir" and <tmp>/packages/loopover-engine/package.json as the sibling, mirroring the
existing injectable-path test pattern already used elsewhere in test/unit/miner-status.test.ts
for readInstalledEnginePackageVersionFromPaths / readExpectedEnginePackageVersionFromPaths),
proving:
readInstalledEnginePackageVersion's catch-all fallback resolves the live monorepo packages/loopover-engine/package.json version correctly when moduleDir() is a dist/lib
style path two levels deeper than packages/loopover-miner/.
readExpectedEnginePackageVersion resolves the live monorepo packages/loopover-engine/package.json
version (not the expected-engine.version pin file) when moduleDir() is that same dist/lib
style path and the live monorepo package.json is present.
Remove the /* v8 ignore next 9 */ annotation on the now-testable catch block once it is
actually covered (it must not remain uncovered-and-ignored after this issue's tests land).
Deliverables
packages/loopover-miner/lib/status.ts's readInstalledEnginePackageVersion catch-all
fallback (currently line 184) computes its workspace-sibling path via resolveMonorepoSiblingPath("..", "loopover-engine", "package.json"), not a hardcoded join(moduleDir(), "../../loopover-engine/package.json").
packages/loopover-miner/lib/status.ts's readExpectedEnginePackageVersion (currently line
222) computes its monorepoEnginePkg argument via resolveMonorepoSiblingPath("..", "loopover-engine", "package.json"), not a hardcoded join(moduleDir(), "../../loopover-engine/package.json").
A new regression test in test/unit/miner-status.test.ts that proves readInstalledEnginePackageVersion's catch-all fallback resolves the correct file from a
simulated dist/lib-depth moduleDir(), named for this issue.
A new regression test in test/unit/miner-status.test.ts that proves readExpectedEnginePackageVersion resolves the live monorepo packages/loopover-engine/package.json
(not the pin file) from a simulated dist/lib-depth moduleDir() when that file is present,
named for this issue.
The /* v8 ignore next 9 */ comment on readInstalledEnginePackageVersion's catch block is
removed once the new test gives it real coverage.
All four Deliverables above are required in the same PR — this is not splittable across follow-ups.
A PR that fixes only one of the two call sites, or adds tests without the corresponding source fix
(or vice versa), does not resolve this issue.
Test Coverage Requirements
packages/loopover-miner/** is measured by codecov/patch (99% target, branch-counted — see packages/loopover-miner/lib/status.ts itself, already covered by test/unit/miner-status.test.ts).
The new tests must exercise both the "found" and "not found" branches of each fixed call site
(mirroring the existing readInstalledEnginePackageVersionFromPaths / readExpectedEnginePackageVersionFromPaths tests' fixture pattern in the same file) so the fix
itself is provably exercised, not just present in source.
Expected Outcome
doctor's engine-version-skew check and status's engine-version display, when run from the real compiled CLI (dist/bin/loopover-miner.js, what actually ships and what the CLI-harness
e2e tests spawn) inside a monorepo checkout, correctly detect the live packages/loopover-engine/package.json version instead of silently falling back to (for readExpectedEnginePackageVersion) the static expected-engine.version pin file or (for readInstalledEnginePackageVersion's catch-all path) null, matching the behavior already correct
today when the same functions run against the .ts source in tests.
Links & Resources
packages/loopover-miner/lib/status.ts (the file to fix — see resolveMonorepoSiblingPath at
lines 62-66 for the already-correct pattern to reuse).
test/unit/miner-status.test.ts (existing test file to extend — see its readInstalledEnginePackageVersionFromPaths / readExpectedEnginePackageVersionFromPaths tests
for the injectable-path fixture pattern to mirror).
Introduced by commit c89679761 ("fix(build): migrate loopover-miner/loopover-mcp to
out-of-place dist/ emit", PR fix(build): migrate loopover-miner/loopover-mcp to out-of-place dist/ emit #8590), which fixed two of four equivalent call sites in the same
file but left these two on the pre-migration hardcoded-depth calculation.
Context
packages/loopover-miner/lib/status.tsresolves the monorepo-siblingpackages/loopover-enginepath relative to its own on-disk location, because this file runs from two different physical
directories depending on context:
packages/loopover-miner/lib/status.ts(source, used byin-process test imports) or
packages/loopover-miner/dist/lib/status.js(the real compiled CLI,since the 2026-07-24 out-of-place
dist/emit migration, commitc89679761, PR #8590).That same commit introduced a depth-flexible helper specifically to handle this:
c89679761's commit message states: "Two production-source bugs found and fixed properly, notpatched around: bin/loopover-mcp.ts and lib/status.ts both compute paths relative to their own
file location ... Both now try the source-relative depth first, falling back to the dist-relative
depth, so they work correctly however they're currently loaded."
That fix was applied to the happy path of
readInstalledEnginePackageVersion(line 180) and tothe pin-file argument of
readExpectedEnginePackageVersion(line 223), but two siblingcall sites that compute the exact same monorepo-sibling path were left on the old
single-hardcoded-depth calculation, which is only correct from the source (
lib/) directory, notfrom the compiled (
dist/lib/) one:readInstalledEnginePackageVersion's catch-all fallback (lib/status.ts:184), reached whenrequireFromHere().resolve(ENGINE_PACKAGE)throws (Node cannot resolve@loopover/engineatall):
readExpectedEnginePackageVersion's first argument (monorepoEnginePkg, lib/status.ts:222):Empirically verified against the built package (
npm --workspace @loopover/engine run build && npm --workspace @loopover/miner run build:tsc, then evaluating both path expressions formoduleDir()=packages/loopover-miner/libvspackages/loopover-miner/dist/lib):lib/(source)dist/lib/(compiled, real CLI)join(moduleDir(), "../../loopover-engine/package.json")packages/loopover-engine/package.json(exists)packages/loopover-miner/loopover-engine/package.json(does not exist)resolveMonorepoSiblingPath("..", "loopover-engine", "package.json")packages/loopover-engine/package.json(exists)packages/loopover-engine/package.json(exists)So when the real compiled CLI (
dist/bin/loopover-miner.js, whatdoctor/statusactually spawnin production and in the CLI-harness e2e tests) runs inside a monorepo checkout:
readExpectedEnginePackageVersion()'smonorepoEnginePkgcheck for the livepackages/loopover-engine/package.jsonalways silently fails (wrong path,existsSyncfalse), so it always falls through to the static
expected-engine.versionpin file instead— even when a live, newer monorepo
packages/loopover-engine/package.jsonis present. Thiscontradicts the function's own doc comment: "Expected minimum engine semver: monorepo engine
package.json when present, else the shipped pin file." The "when present" branch is dead code
from the compiled CLI's perspective.
readInstalledEnginePackageVersion()'s catch-all fallback (the last-resort path used when@loopover/enginecannot be resolved viarequire.resolveat all) computes the wrong monoreposibling path from
dist/lib/, so it returnsnullinstead of finding the real workspaceversion — again, only from the compiled CLI's execution context.
This is a genuine regression introduced by
c89679761(verified: before that migration, sourceand compiled output shared the same directory —
packages/loopover-miner/lib/status.tsnext toits in-place-emitted
status.js— sojoin(moduleDir(), "../../loopover-engine/package.json")was correct from either execution mode; the
dist/split broke that assumption for these twocall sites only). Neither is covered by the existing test suite:
test/unit/miner-status.test.tsonly callsreadInstalledEnginePackageVersionFromPaths/readExpectedEnginePackageVersionFromPathsdirectly with injected paths, or calls thezero-arg wrapper functions from the
.tssource context (wheremoduleDir()always resolves tolib/, neverdist/lib/) — so the compiled-CLI code path these two spots are meant to serve iscompletely unexercised, matching the
/* v8 ignore next 9 */annotation already sitting on thecatch block.
Requirements
packages/loopover-miner/lib/status.tswith the already-existingresolveMonorepoSiblingPathhelper, exactly the same way the other two call sites in the same file were already fixed:
readInstalledEnginePackageVersion's catch-all fallback (theworkspacePkglocal computed atline 184) must use
resolveMonorepoSiblingPath("..", "loopover-engine", "package.json")instead of
join(moduleDir(), "../../loopover-engine/package.json").readExpectedEnginePackageVersion's first argument (line 222) must useresolveMonorepoSiblingPath("..", "loopover-engine", "package.json")instead ofjoin(moduleDir(), "../../loopover-engine/package.json").lib/)execution context — both must continue to resolve the identical file they already resolve today
when
moduleDir()ispackages/loopover-miner/lib.dist/lib/directory depth(i.e. construct a temporary directory tree with
<tmp>/packages/loopover-miner/dist/lib/as the"module dir" and
<tmp>/packages/loopover-engine/package.jsonas the sibling, mirroring theexisting injectable-path test pattern already used elsewhere in
test/unit/miner-status.test.tsfor
readInstalledEnginePackageVersionFromPaths/readExpectedEnginePackageVersionFromPaths),proving:
readInstalledEnginePackageVersion's catch-all fallback resolves the live monorepopackages/loopover-engine/package.jsonversion correctly whenmoduleDir()is adist/libstyle path two levels deeper than
packages/loopover-miner/.readExpectedEnginePackageVersionresolves the live monorepopackages/loopover-engine/package.jsonversion (not the
expected-engine.versionpin file) whenmoduleDir()is that samedist/libstyle path and the live monorepo package.json is present.
/* v8 ignore next 9 */annotation on the now-testable catch block once it isactually covered (it must not remain uncovered-and-ignored after this issue's tests land).
Deliverables
packages/loopover-miner/lib/status.ts'sreadInstalledEnginePackageVersioncatch-allfallback (currently line 184) computes its workspace-sibling path via
resolveMonorepoSiblingPath("..", "loopover-engine", "package.json"), not a hardcodedjoin(moduleDir(), "../../loopover-engine/package.json").packages/loopover-miner/lib/status.ts'sreadExpectedEnginePackageVersion(currently line222) computes its
monorepoEnginePkgargument viaresolveMonorepoSiblingPath("..", "loopover-engine", "package.json"), not a hardcodedjoin(moduleDir(), "../../loopover-engine/package.json").test/unit/miner-status.test.tsthat provesreadInstalledEnginePackageVersion's catch-all fallback resolves the correct file from asimulated
dist/lib-depthmoduleDir(), named for this issue.test/unit/miner-status.test.tsthat provesreadExpectedEnginePackageVersionresolves the live monorepopackages/loopover-engine/package.json(not the pin file) from a simulated
dist/lib-depthmoduleDir()when that file is present,named for this issue.
/* v8 ignore next 9 */comment onreadInstalledEnginePackageVersion's catch block isremoved once the new test gives it real coverage.
All four Deliverables above are required in the same PR — this is not splittable across follow-ups.
A PR that fixes only one of the two call sites, or adds tests without the corresponding source fix
(or vice versa), does not resolve this issue.
Test Coverage Requirements
packages/loopover-miner/**is measured bycodecov/patch(99% target, branch-counted — seepackages/loopover-miner/lib/status.tsitself, already covered bytest/unit/miner-status.test.ts).The new tests must exercise both the "found" and "not found" branches of each fixed call site
(mirroring the existing
readInstalledEnginePackageVersionFromPaths/readExpectedEnginePackageVersionFromPathstests' fixture pattern in the same file) so the fixitself is provably exercised, not just present in source.
Expected Outcome
doctor'sengine-version-skewcheck andstatus's engine-version display, when run from thereal compiled CLI (
dist/bin/loopover-miner.js, what actually ships and what the CLI-harnesse2e tests spawn) inside a monorepo checkout, correctly detect the live
packages/loopover-engine/package.jsonversion instead of silently falling back to (forreadExpectedEnginePackageVersion) the staticexpected-engine.versionpin file or (forreadInstalledEnginePackageVersion's catch-all path)null, matching the behavior already correcttoday when the same functions run against the
.tssource in tests.Links & Resources
packages/loopover-miner/lib/status.ts(the file to fix — seeresolveMonorepoSiblingPathatlines 62-66 for the already-correct pattern to reuse).
test/unit/miner-status.test.ts(existing test file to extend — see itsreadInstalledEnginePackageVersionFromPaths/readExpectedEnginePackageVersionFromPathstestsfor the injectable-path fixture pattern to mirror).
c89679761("fix(build): migrate loopover-miner/loopover-mcp toout-of-place dist/ emit", PR fix(build): migrate loopover-miner/loopover-mcp to out-of-place dist/ emit #8590), which fixed two of four equivalent call sites in the same
file but left these two on the pre-migration hardcoded-depth calculation.