What happened?
After migrating to pnpm 11, launching pi repeatedly runs global pnpm install operations for every package declared in ~/.pi/agent/settings.json under packages.
The packages are already installed according to pnpm list -g --depth 0, but Pi re-runs the package bootstrap on every startup.
settings.json
{
"lastChangelogVersion": "0.74.0",
"packages": [
"npm:pi-web-access",
"npm:@plannotator/pi-extension",
"npm:pi-subagents",
"npm:pi-lens",
"npm:pi-tool-display",
"npm:pi-powerline-footer"
],
"npmCommand": ["pnpm"]
}
Startup output
On each pi launch, Pi runs global installs sequentially:
Packages: +21
Progress: resolved 21, reused 21, downloaded 0, added 0, done
global:
+ pi-web-access 0.10.7
Done in 990ms using pnpm v11.1.1
Packages: +297
Progress: resolved 306, reused 297, downloaded 0, added 0, done
global:
+ @plannotator/pi-extension 0.19.14
Done in 2s using pnpm v11.1.1
Packages: +3
Progress: resolved 3, reused 3, downloaded 0, added 0, done
global:
+ pi-subagents 0.24.2
Done in 468ms using pnpm v11.1.1
Packages: +247
Progress: resolved 264, reused 247, downloaded 0, added 0, done
global:
+ pi-lens 3.8.43
Done in 1.7s using pnpm v11.1.1
Packages: +244
Progress: resolved 253, reused 244, downloaded 0, added 0, done
global:
+ pi-tool-display 0.3.6
Done in 1.2s using pnpm v11.1.1
Packages: +242
Progress: resolved 251, reused 242, downloaded 0, added 0, done
global:
+ pi-powerline-footer 0.5.4
Done in 1.1s using pnpm v11.1.1
Total: ~7.5s of no-op package resolution on every startup.
Already installed globally
$ pnpm list -g --depth 0
/home/imac/.local/share/pnpm/global/v11 (PRIVATE)
dependencies:
@earendil-works/pi-coding-agent@0.74.0
@plannotator/pi-extension@0.19.14
pi-lens@3.8.43
pi-powerline-footer@0.5.4
pi-subagents@0.24.2
pi-tool-display@0.3.6
pi-web-access@0.10.7
Root cause
Pi's package-manager.ts → getNpmInstallPath() joins getGlobalNpmRoot() (which runs pnpm root -g) with source.name, then resolvePackageSources() checks existsSync(installedPath).
With pnpm 11, pnpm root -g returns:
/home/imac/.local/share/pnpm/global/v11
Pi constructs and checks:
/home/imac/.local/share/pnpm/global/v11/pi-web-access ← does not exist
But pnpm 11 stores packages in content-hashed directories:
/home/imac/.local/share/pnpm/global/v11/10c947-19e23d3dc9b/node_modules/pi-web-access
So existsSync() always returns false → needsInstall = true → pnpm install -g fires for every package on every startup.
The global/v11/ directory contains hashed dirs and symlinks, not a flat node_modules/<name> tree:
/home/imac/.local/share/pnpm/global/v11/
├── 0108ced76e89ba0de26e5dc78c03480eecf6f536153b5e5951240be7397a7b68 -> 10c95a-19e23d3e0ac
├── 10c947-19e23d3dc9b/
├── 10c95a-19e23d3e0ac/
├── 10c96e-19e23d3e91c/
├── 10c980-19e23d3eb34/
├── 10c9a6-19e23d3f262/
├── 10c9c2-19e23d3f77a/
├── pnpm-workspace.yaml
└── ... (symlinks to hashed dirs)
Each hashed dir contains its own node_modules/ with the package inside — there is no top-level node_modules/ flat tree that the npm-style path construction expects.
Environment
pi: 0.74.0
node: v24.15.0
pnpm: 11.1.1
pnpm bin -g: /home/imac/.local/share/pnpm/bin
pnpm root -g: /home/imac/.local/share/pnpm/global/v11
shell: bash
OS: Ubuntu/Linux
Expected behavior
If the npm packages listed in settings.json.packages are already installed globally and resolvable, Pi should not run pnpm install -g for each package on every startup.
Possible fix
getGlobalNpmRoot() / getNpmInstallPath() could use pnpm list -g --json or require.resolve() to locate an already-installed package instead of constructing an npm-style path that does not exist in pnpm 11's layout. This is similar to how the bun path was fixed in #3809 — the global root resolution logic needs a pnpm-specific branch that accounts for pnpm 11's content-addressable global layout.
Related issues
What happened?
After migrating to pnpm 11, launching
pirepeatedly runs globalpnpm installoperations for every package declared in~/.pi/agent/settings.jsonunderpackages.The packages are already installed according to
pnpm list -g --depth 0, but Pi re-runs the package bootstrap on every startup.settings.json
{ "lastChangelogVersion": "0.74.0", "packages": [ "npm:pi-web-access", "npm:@plannotator/pi-extension", "npm:pi-subagents", "npm:pi-lens", "npm:pi-tool-display", "npm:pi-powerline-footer" ], "npmCommand": ["pnpm"] }Startup output
On each
pilaunch, Pi runs global installs sequentially:Total: ~7.5s of no-op package resolution on every startup.
Already installed globally
Root cause
Pi's
package-manager.ts→getNpmInstallPath()joinsgetGlobalNpmRoot()(which runspnpm root -g) withsource.name, thenresolvePackageSources()checksexistsSync(installedPath).With pnpm 11,
pnpm root -greturns:Pi constructs and checks:
But pnpm 11 stores packages in content-hashed directories:
So
existsSync()always returnsfalse→needsInstall = true→pnpm install -gfires for every package on every startup.The
global/v11/directory contains hashed dirs and symlinks, not a flatnode_modules/<name>tree:Each hashed dir contains its own
node_modules/with the package inside — there is no top-levelnode_modules/flat tree that the npm-style path construction expects.Environment
Expected behavior
If the npm packages listed in
settings.json.packagesare already installed globally and resolvable, Pi should not runpnpm install -gfor each package on every startup.Possible fix
getGlobalNpmRoot()/getNpmInstallPath()could usepnpm list -g --jsonorrequire.resolve()to locate an already-installed package instead of constructing an npm-style path that does not exist in pnpm 11's layout. This is similar to how the bun path was fixed in #3809 — the global root resolution logic needs a pnpm-specific branch that accounts for pnpm 11's content-addressable global layout.Related issues
getGlobalNpmRoot()does not work withbunasnpmCommand#3809 —getGlobalNpmRoot()does not work withbunasnpmCommand