What
In an isolated home whose settings.json declares gentle-pi as a path package, passing --package-root <different-dir> still forces the take-over and injects that requested root — while bin/gentle-shell.mjs prints a warning saying the flag is ignored.
Why it happens
The clause that looks like it decides is not the one that decides. decideTakeOver short-circuits on packageRootExplicit, but that boolean is only forwarded as true in --link mode:
// bin/gentle-shell.mjs:1193
takeOver = decideTakeOver({
declaration,
realPackageRoot: realEffectivePackageRoot, // ← realpath of the REQUESTED root
realDeclaredDir,
packageRootExplicit: home.mode === "link" && packageRootExplicit,
});
The requested root reaches that call through effectivePackageRoot:
// bin/gentle-shell.mjs:1145
const effectivePackageRoot = packageRootExplicit ? resolvePath(args.packageRoot) : packageRoot;
// bin/gentle-shell.mjs:1191
const realEffectivePackageRoot = safeRealpath(effectivePackageRoot);
and the path clause compares the declared dir against it:
// runtime/gentle-shell-launcher.mjs:635
export function decideTakeOver(input) {
if (input.packageRootExplicit) return true;
if (input.declaration === undefined) return false;
if (input.declaration.kind === "npm") return false;
const realDeclaredDir = input.realDeclaredDir ?? input.declaration.dir;
return realDeclaredDir !== input.realPackageRoot; // ← declared dir vs REQUESTED root
}
On take-over the injected root is the requested one (bin:1226 passes packageRoot: effectivePackageRoot to buildPiInvocation), so the flag genuinely took effect.
Meanwhile the warning fires on a condition that is only about the flag being present, the mode not being --link, and a declaration existing — evaluated before the decision:
// bin/gentle-shell.mjs:1186
if (packageRootExplicit && home.mode !== "link" && declaration !== undefined) {
process.stderr.write(`gentle-shell: --package-root only forces a take-over in --link mode; ${home.dir} declares gentle-pi, so the installed package is used and ${args.packageRoot} is ignored\n`);
}
The comment above it states the same thing and is wrong the same way: "in every other mode a declared home silently keeps using its declared gentle-pi and --package-root has no effect at all". That is true for an npm: declaration, and for a path: declaration only while the requested root resolves to the declared one.
Reproduction
buildPiInvocation and decideTakeOver imported from the real launcher and driven with these inputs (isolated home, home.mode !== "link", packageRootExplicit forwarded as home.mode === "link" && args.packageRoot !== undefined):
| # |
declaration |
--package-root |
takeOver |
injected args |
| 1 |
npm:gentle-pi@3.5.1 |
R |
false |
[] |
| 2 |
path: = launcher's own root |
R (different) |
true |
["--no-extensions","-e",R] |
| 3 |
path: = launcher's own root |
none |
false |
[] |
| 4 |
path: = another dir |
none |
true |
["--no-extensions","-e",<own root>] |
| 5 |
no declaration |
R |
false |
["-e",R] |
| 6 |
--link, path: = launcher's own root |
R (different) |
true |
["--no-extensions","-e",R] |
Row 2 is the defect: the flag forced the take-over and its root was injected, while stderr said it was ignored. Row 5 shows the comment's "no effect at all" is only true of declared homes — without a declaration the flag still selects which root is loaded.
The decision columns are identical on the installed gentle-pi@3.7.0 and on main at fd050add; the two differ only in the asset arguments, which is expected (the 3.7.0 release predates 0031c09).
Expected
One of:
- Documentation-only: the warning and the code comment should describe the actual rule — the flag is ignored outright for an
npm: declaration, and for a path: declaration only while the requested root resolves to the declared package root. Row 2 then needs no warning at all.
- Behaviour change: forward
packageRootExplicit in every mode so the flag's effect matches what the warning already claims. This changes what an isolated home with a path declaration does when the flag is passed, so it needs a maintainer decision rather than a drive-by.
Option 1 leaves the code as the source of truth; option 2 makes the warning honest by changing behaviour. Either way the current combination is inconsistent.
Coverage gap
tests/gentle-shell-bin.test.ts covers an npm: declaration (:1864, which returns false before the path clause) and a run with no declaration (:2311). Isolated + path: declaration + --package-root has no test, which is why the mismatch survives.
Provenance
Code read at fd050add; bin/gentle-shell.mjs was last changed 2026-09-22 (5b9d6a7b), before the v3.7.0 tag, and the launcher behaviour described here is present in the published 3.7.0 package. Found while correcting a documentation sentence in #1486.
What
In an isolated home whose
settings.jsondeclaresgentle-pias a path package, passing--package-root <different-dir>still forces the take-over and injects that requested root — whilebin/gentle-shell.mjsprints a warning saying the flag is ignored.Why it happens
The clause that looks like it decides is not the one that decides.
decideTakeOvershort-circuits onpackageRootExplicit, but that boolean is only forwarded as true in--linkmode:The requested root reaches that call through
effectivePackageRoot:and the path clause compares the declared dir against it:
On take-over the injected root is the requested one (
bin:1226passespackageRoot: effectivePackageRoottobuildPiInvocation), so the flag genuinely took effect.Meanwhile the warning fires on a condition that is only about the flag being present, the mode not being
--link, and a declaration existing — evaluated before the decision:The comment above it states the same thing and is wrong the same way: "in every other mode a declared home silently keeps using its declared gentle-pi and --package-root has no effect at all". That is true for an
npm:declaration, and for apath:declaration only while the requested root resolves to the declared one.Reproduction
buildPiInvocationanddecideTakeOverimported from the real launcher and driven with these inputs (isolated home,home.mode !== "link",packageRootExplicitforwarded ashome.mode === "link" && args.packageRoot !== undefined):--package-roottakeOvernpm:gentle-pi@3.5.1false[]path:= launcher's own roottrue["--no-extensions","-e",R]path:= launcher's own rootfalse[]path:= another dirtrue["--no-extensions","-e",<own root>]false["-e",R]--link,path:= launcher's own roottrue["--no-extensions","-e",R]Row 2 is the defect: the flag forced the take-over and its root was injected, while stderr said it was ignored. Row 5 shows the comment's "no effect at all" is only true of declared homes — without a declaration the flag still selects which root is loaded.
The decision columns are identical on the installed
gentle-pi@3.7.0and onmainatfd050add; the two differ only in the asset arguments, which is expected (the3.7.0release predates0031c09).Expected
One of:
npm:declaration, and for apath:declaration only while the requested root resolves to the declared package root. Row 2 then needs no warning at all.packageRootExplicitin every mode so the flag's effect matches what the warning already claims. This changes what an isolated home with a path declaration does when the flag is passed, so it needs a maintainer decision rather than a drive-by.Option 1 leaves the code as the source of truth; option 2 makes the warning honest by changing behaviour. Either way the current combination is inconsistent.
Coverage gap
tests/gentle-shell-bin.test.tscovers annpm:declaration (:1864, which returnsfalsebefore the path clause) and a run with no declaration (:2311). Isolated +path:declaration +--package-roothas no test, which is why the mismatch survives.Provenance
Code read at
fd050add;bin/gentle-shell.mjswas last changed 2026-09-22 (5b9d6a7b), before thev3.7.0tag, and the launcher behaviour described here is present in the published3.7.0package. Found while correcting a documentation sentence in #1486.