Gentle Shell installs as one self-contained product. It carries its own pinned Node (with the npm in the same archive) and pnpm, and Go only when a build needs it. Pi is pinned with each Gentle Shell release. The user's own node, npm, pnpm and go are never run, read or changed.
The building blocks live in scripts/bundled-install.mjs. The web installer and gentle-shell upgrade share them. The web installer uses them for a new installation of the release channel (bundled installation). The launcher and gentle-shell upgrade use them in a bundled install (Launcher in a bundled install).
| Path (POSIX) | Contents |
|---|---|
~/.gentle-shell/agent/ |
The isolated agent home. This module never writes it. |
~/.gentle-shell/runtime/node-24.21.0/ |
Our Node from the verified archive: bin/node, the npm bundled with it under lib/node_modules/npm, our own bin/npm and bin/npx wrappers, and gentle-shell/npm, the entry Pi's npmCommand runs. |
~/.gentle-shell/npm/{prefix,cache}/, ~/.gentle-shell/npm/npmrc |
npm's global prefix, cache and global config. The global config file is never written. |
~/.gentle-shell/runtime/pnpm-11.1.1/package/ |
Our pnpm, from the verified registry tarball. |
~/.gentle-shell/runtime/go/1.25.14/go/ |
Our Go, only when asked (acquireGo layout). |
~/.gentle-shell/runtime/engram-3.5.2/engram |
Our Engram, from the verified GitHub release archive (Isolation from the user's Gentle AI). |
~/.gentle-shell/gentle-ai/ |
Gentle AI's state, managed assets, backups and telemetry state (GENTLE_AI_HOME). |
~/.gentle-shell/config/gentle-ai/ |
gentle-pi's config home in a bundled install (derived, never exported to Pi): profiles, preferences, persona, background subagents. |
~/.gentle-shell/pnpm/{store,cache,state,config}/ |
Every folder pnpm writes. |
~/.gentle-shell/versions/<shell>-<pi>/ |
One frozen install per release. |
~/.gentle-shell/current |
Symlink to versions/<id>, switched with one rename. |
~/.gentle-shell/versions.history |
Activation order, newest first, for keep 2. |
~/.gentle-shell/bin/gentle-shell |
The launcher. |
On Windows the same layout lives under a private folder. The module tries %LOCALAPPDATA%\gentle-shell first. It falls back to %USERPROFILE%\.gentle-shell-bundle only when an untrusted owner or ACL rejects the first folder, the same rule the bootstrap uses for its tools. The folder is claimed with the bootstrap's protected DACL and marked with .gentle-shell-bundle (ensureWindowsPrivateFolder). current is a pointer file holding the version id. The launcher is bin\gentle-shell.cmd, and tmp\ holds TEMP and TMP for pnpm's children. The agent home stays at %USERPROFILE%\.gentle-shell\agent.
windowsProcessCheck still throws exactly Windows prerequisite process failed
(process-failed classification). Its standard Error cause preserves status,
signal and selected process-error fields plus bounded stdout/stderr. A sanitized
cause.stack also carries these fields for existing evidence consumers, without
local callsites, raw spawn errors, commands, argv or environment snapshots.
Process-error messages use only a sanitized native error code or fixed fallback,
never raw spawn text that could include a private bare executable name.
Limits are UTF-16 code units: streams1024 each, error message256, name/code/errno
text64, signal32 and cause stack8192; numeric status/errno remain numeric.
Path/SID, encoded ACL/opaque and control-bearing lines are suppressed; credential
markers, including underscore-separated environment names such as GITHUB_TOKEN
and API_KEY, suppress the entire field including continuation lines. This conservative
filter may discard useful output and is not a general-purpose secret parser.
Nothing prints automatically. CMD/BAT rejection, neutral cwd, shell=false,
15-second SIGKILL/1MiB capture and all ACL/provenance checks remain unchanged.
Elapsed time alone does not establish timeout or diagnose the historical CI failure.
- Runtimes. Each one is downloaded through
verifiedDownload(the existing pins and hashes), written to a staging folder and run once to check its version. It is then marked with.gentle-shell-runtimeand published with one rename. A marked runtime is reused. An unmarked folder is never replaced. Renaming is safe here because the extracted trees hold only regular files and folders, never links. The pnpm tarball's executable bits (bin/pnpm.mjs,dist/node-gyp-bin/node-gyp) are kept, for the owner only. - Versions. Each version is installed in place, in its final
versions/<shell>-<pi>/folder, and that folder is never renamed afterwards. On Windows, when symlinks are not allowed, pnpm links packages with junctions to absolute paths, so a renamed folder would leave every link dangling. The folder is claimed withmkdir, and its.gentle-shell-versionmarker first readsinstalling <id> <pid>. After pnpm runs, the installedgentle-piand Pi versions must match the manifest exactly. Only then is the marker replaced, with one rename, by<id>. Only that final marker makes the version valid. - Interrupted installs. A folder whose marker is
installing <id> <pid>for this id, and whose process is gone, is removed and installed again. A live process stops the install. Anything else at that path is never replaced. A failed install removes the folder it claimed. - Go on Windows.
installVersionrefuses to run on Windows without the pinned Go underruntime/go. gentle-pi's postinstall builds Gentle AI with the firstgo.exeon PATH, which must never be the user's. current. Only our own symlink or pointer file is switched. Anything else at that path stops the switch.- Pruning.
pruneVersions(layout, 2)keeps the active version and the most recently activated other one. It deletes nothing unlesscurrentnames an installed version. It never deletes the active version, an unfinished install, or a folder without our marker. - Launcher. Only a launcher that carries our generated marker line is refreshed. Any other file there is never replaced. The launcher passes every argument and the exit code through. When no active version exists, it prints
gentle-shell: no active Gentle Shell version; run the Gentle Shell installer again.and exits 1. The Windows.cmdruns with delayed expansion off, so!,%,^,&,(and,in the prefix path stay literal.
pnpmEnvironment(layout) builds the environment for our pnpm and the scripts it runs. It was checked against the pnpm 11.1.1 sources, where config/reader reads pnpm_config_* variables, the XDG folders, and npmrc_auth_file in place of ~/.npmrc.
| Setting | Value |
|---|---|
PATH |
Our Node's folder first, then our Go's when given. pnpm runs lifecycle scripts with the node found on PATH (scripts-prepend-node-path is false). |
PNPM_HOME |
pnpm/ |
pnpm_config_store_dir, pnpm_config_cache_dir, pnpm_config_state_dir |
pnpm/store, pnpm/cache, pnpm/state |
XDG_CONFIG_HOME, XDG_CACHE_HOME, XDG_STATE_HOME |
pnpm/config, pnpm/cache, pnpm/state. pnpm reads its global config.yaml and auth.ini only from the config folder. |
pnpm_config_npmrc_auth_file |
pnpm/config/npmrc, read in place of the user's ~/.npmrc. It is empty until the installer copies the user's network, registry and authentication keys into it (User npm settings). |
pnpm_config_pm_on_fail |
ignore. Its default, download, makes pnpm switch to the pnpm that a packageManager field in a package.json above the folder names. Observed: pnpm --version under such a folder printed 9.0.0 instead of 11.1.1. |
pnpm_config_runtime_on_fail |
ignore, so a root manifest never makes pnpm download another Node. |
pnpm_config_update_notifier |
false |
TEMP, TMP (Windows) |
tmp\ |
Every inherited npm_*, pnpm_* (including PNPM_HOME), COREPACK_*, NODE_OPTIONS, NODE_PATH and XDG folder variable is dropped. The exception is a variable the prefix npmrc names as ${NAME}, such as NPM_TOKEN: pnpm expands it, so it is kept. Everything else, such as HOME, proxies and SystemRoot, is kept.
distributionFiles(manifest) returns the exact project that the release lockfile is resolved from and installed with:
package.jsonpinsgentle-piand@earendil-works/pi-coding-agentexactly.pnpm-workspace.yamlsetsallowBuilds: { gentle-pi: true }andstrictDepBuilds: false.
These settings mean:
- Only gentle-pi's build scripts run. This matches today's
--allow-build=gentle-pi. That flag belongs topnpm addonly; forpnpm installthe allowlist lives inpnpm-workspace.yaml. - Other build scripts are skipped, not fatal. Pi's tree has build scripts (
@google/genai,koffi,protobufjs), and pnpm 11 fails on ignored builds by default. - The parent folders are ignored. The workspace file also stops pnpm's upward search, so a
pnpm-workspace.yamlor.npmrcin a parent folder never applies.
installVersion then runs node pnpm.mjs install --frozen-lockfile in the version folder.
After an install that skips build scripts, pnpm 11 rewrites pnpm-workspace.yaml in place. It adds each skipped package under allowBuilds with the placeholder value set this to true or false. The release lockfile is therefore resolved from the unmodified distributionFiles() output. It never comes from a pnpm-workspace.yaml read back from a folder pnpm already installed into.
Each release publishes two assets with stable names. scripts/build-distribution.mjs builds and verifies them.
| Asset | Contents |
|---|---|
gentle-shell-distribution.json |
schema (1), shell (gentle-pi), pi, id (<shell>-<pi>), generatedWith (our Node and pnpm pins), files (the exact package.json and pnpm-workspace.yaml that installVersion writes) and lockfile (name and sha256). |
gentle-shell-distribution-lock.yaml |
The pnpm-lock.yaml for those files. |
- Exact versions.
shellis the release version.piisPI_INSTALL_VERSIONfromscripts/installer-preflight.mjs, the one Pi pin. A range, a tag such aslatest, avprefix or build metadata is refused before pnpm runs. - Resolution.
buildDistributionwritesdistributionFiles()into a fresh folder under the prefix'stmp/and runs our pinned pnpm withinstall --lockfile-onlyinpnpmEnvironment(layout), the environmentinstallVersionuses. Onlypnpm-lock.yamlis read back. - Just published. The registry may not list
gentle-pi@<version>for a few minutes afternpm publish. When pnpm reportsERR_PNPM_NO_MATCHING_VERSIONfor exactly that gentle-pi (pnpm prints it on stdout), the resolution is retried in a fresh folder: 10 attempts, 30 seconds apart, about five minutes in all. It then fails withgentle-pi@<version> is not on the registry after 10 attempts. Any other failure, including a missing Pi, stops at once. - Deterministic.
distributionAsset(manifest, lockfile)is plain data in a fixed order, so the same inputs give the same bytes. The lockfile itself records the registry at resolution time, which is why it is built once per release and then only installed. - Reading.
readDistribution(json, lockfile)returns{ manifest, lockfile }only when the versions are exact, the lockfile matches the recordedsha256, andfilesequals what this module'sinstallVersionwrites.generatedWithis recorded and not compared.
pnpm resolves every optional dependency into the lockfile, with its os, cpu and libc fields, whatever system resolves it. Only the install skips the packages for other systems. So no supportedArchitectures setting is needed, and none is set.
Observed with pnpm 11.1.1 for gentle-pi 4.0.0 and Pi 1.0.0:
- A lockfile resolved on macOS arm64 lists every
@yuuang/ffi-rs-*,@ff-labs/fff-bin-*and@esbuild/*variant for Linux (gnu and musl), Windows and macOS. - A
--frozen-lockfileinstall of that lockfile on macOS withsupportedArchitecturesforced to Linux and Windows installed the Linux and Windows variants. The lockfile was left unchanged. - The regular frozen install on macOS installed only the darwin-arm64 variants.
pnpm 11.1.1 defaults minimum-release-age to 1440 minutes (one day), in non-strict mode. The default is kept on purpose, and no value is set:
- Ranged dependencies. For a ranged transitive dependency, pnpm picks the highest matching version that is at least one day old. A version published minutes before a release does not enter the lockfile. This is a supply-chain guard.
- Exact pins. gentle-pi and Pi are exact. In non-strict mode, when no matching version is old enough, pnpm falls back to the matching version anyway (
pickRespectingMinReleaseAge). So a gentle-pi published minutes ago still resolves. - Never set it explicitly. pnpm 11.1.1 turns
minimumReleaseAgeStricton wheneverminimumReleaseAgeis set explicitly and the strict flag is not. A strict check would refuse the gentle-pi the release just published.
Observed with pnpm 11.1.1:
pnpm_config_minimum_release_age=5256000alone failed.- The same value with
pnpm_config_minimum_release_age_strict=falseresolved gentle-pi 4.0.0 exactly. - The default resolved it too.
pnpmEnvironment drops every inherited pnpm_* variable, so a host setting cannot change this.
.github/workflows/publish.yml runs three jobs after publish, because gentle-pi@<version> must resolve from npm first:
-
distributionchecks out the verified release commit. It runsbuild-distribution.mjs build --shell <tag without v> --out <dir>with our pinned Node and pnpm in a temporary prefix, and keeps both files as a workflow artifact. -
distribution-verifyruns onubuntu-latest,macos-latestandwindows-latest. It runsbuild-distribution.mjs verify --assets <dir>, which:- downloads our pinned Node and pnpm into a temporary prefix (with Go on Windows);
- installs the exact pair with
installVersion(--frozen-lockfile); - switches
currentand writes the launcher; - requires
gentle-shell --versionto report exactlygentle-shell <shell>andpi <pi>.
On Windows a fresh temporary folder stands in for the private-folder claim, which has its own native tests. The launcher runs in
pnpmEnvironment, the environment our pnpm ran in, without anyPI_*orGENTLE_*variable. So no hostNODE_OPTIONS,NODE_PATH, npm or pnpm setting, or Pi override reaches it. -
distribution-assetsruns only after every verify lane passed. It attaches both files to the release withgh release upload --clobber.
Both build and verify create their temporary prefix (gsd-* in the system temporary folder) only after their arguments are accepted. They always remove it, whether the command succeeded or failed.
The release gets the same bytes that the three systems installed.
pnpm 11 runs on Node alone: bin/pnpm.mjs, with node-gyp bundled in its dist/. Pi needs npm, though. Pi 1.1.0's package manager runs npm install <spec> --prefix <agent dir>/npm, and gentle-shell setup runs pi install npm:<package> through Gentle AI.
So nodeRuntimeFiles takes the npm bundled in the same verified Node archive (npm 11.19.0 for Node 24.21.0):
- POSIX.
lib/node_modules/npm/, plus our ownbin/npmandbin/npx. These are small sh wrappers that run ournodewith npm's entry, in place of the archive's links. - Windows.
node_modules\npm\withnpm.cmdandnpx.cmd. These run thenode.exenext to them. - Skipped. Links, Corepack and every other member. A member that would leave its folder, or an archive without npm, is rejected.
ensureRuntime runs npm-cli.js --version with our Node from the staging folder. It must print the version in npm's own package.json.
ensureNpmShim then writes runtime/node-<v>/gentle-shell/npm, a CommonJS file that loads the npm next to it, and a package.json beside it with "type": "commonjs". That package.json keeps the entry CommonJS even under a user's package.json with "type": "module" above the prefix. ensureRuntime writes the entry for a new runtime and for a reused one. The launcher also writes it before it sets npmCommand, for a runtime installed before the entry existed. Each file is replaced with one rename, and only when its content differs. A link or anything but a regular file in its place stops it. A runtime from before npm was published (T1a) gets no entry, and the launcher then gives Pi no npmCommand and says so.
Why the entry exists:
- The name. Pi 1.1.0 takes the package manager's name from the basenames in
npmCommand(getPackageManagerName), and only strips.cmdand.exe. Withnpm-cli.jsit readnode, and gave a git extension's dependencies a plainnpm install. With an entry named exactlynpm, it readsnpmand usesinstall --omit=dev --legacy-peer-deps. - No
npm.cmd. On Windows,npm.cmdprefers an npm in the global prefix, which is the user's under the user's environment. The entry never goes through it.
userNpmrc reads the user's npm configuration: NPM_CONFIG_USERCONFIG (any case) when it is an absolute path, otherwise ~/.npmrc. It reads at most 1 MiB.
filterNpmrc keeps only these keys (S12):
registryand@scope:registry;- per host:
//host/:_authToken,_auth,username,_password,certfileandkeyfile; proxy,https-proxy,noproxyandno-proxy;ca(andca[]),cafileandstrict-ssl.
Each kept line is copied verbatim, so ${VAR} references stay for pnpm and npm to expand. Comments, CRLF line ends and quoted keys are handled. Every other key is dropped, and so is everything from the first [section] on: an ini section never applies at the top level. The frozen lockfile still pins every version and integrity hash, so these keys cannot change what is installed.
writeNpmrcAuth writes the result to pnpm/config/npmrc with one rename, mode 0600. On Windows that file is inside the private folder.
setupEnvironment(layout, { platform, env, id }) is the environment the web installer runs gentle-shell setup in. The launcher itself then gives its children the user's environment (Launcher in a bundled install). Its PATH, in order:
- Our Node's folder, which holds npm on POSIX and
npm.cmdon Windows. versions/<id>/node_modules/.bin, which holds the version'spi.- The user's PATH.
npm gets its settings inside the prefix:
npm_config_userconfigis the filtered prefix npmrc.npm_config_globalconfig,npm_config_prefixandnpm_config_cachepoint under~/.gentle-shell/npm/.npm_config_update_notifierisfalse.
The global prefix also keeps Windows' npm.cmd on our npm. That shim prefers an npm installed in the global prefix.
Dropped: inherited npm and pnpm settings, NODE_OPTIONS, NODE_PATH, and the wizard's GENTLE_BOOTSTRAP_* and GENTLE_INSTALL_* handoff. The user's XDG folders are kept, because Pi and Gentle AI read their own configuration through them.
pnpmEnvironment does not set npm's settings, because pnpm never runs npm. The release verification also requires its launcher environment to hold no npm_config_prefix.
pathEntryPlan returns the single PATH change and does not apply it:
- Symlink. If
~/.local/binis on PATH, the plan is a~/.local/bin/gentle-shellsymlink, when that name is free. - Profile line. Otherwise the plan is one line ending in
# gentle-shell bundled install, added to the file the user's shell reads:$ZDOTDIR/.zshrc(or~/.zshrc) for zsh.- For bash on macOS, where terminals start login shells: the first of
~/.bash_profile,~/.bash_loginand~/.profilethat exists. Login bash reads exactly one of them, in that order (INVOCATIONin bash(1)).~/.bash_profileis created only when none exists, because creating it would hide the user's~/.profile. - For bash elsewhere:
~/.bashrc, which interactive non-login shells (Linux terminals) read. Distributions'~/.profilesources it for login shells. - Our own
conf.d/gentle-shell.fishfor fish. ~/.profileotherwise.
- Manual. The installer edits that file only when it is a regular file this user can write and every folder above it under home is real. It may also create the file when it is absent and its folders are real and writable. A symlinked profile (stow, chezmoi, home-manager), a read-only one, anything that is not a file, or a file reached through a linked folder gets a
manualplan instead. That plan carries the exact line to add, and nothing is written. When the file, even through a link, already holds the line, the plan isnone, so the user is not asked twice. - Registry (Windows). The plan is the
binentry in the HKCU userPath.
applyPathEntry applies only that change and returns a receipt. The receipt records whether a newline was added before our line and whether the file was created. removePathEntry(receipt) uses it to restore the file byte for byte, and deletes a file it created once nothing else is left in it. A receipt with applied: false (the line, the link or the Windows Path entry was already there) is never reverted.
-
Persisted receipt. The
bundled-pathstep (a new install and a migration alike) writes an applied receipt to<prefix>/path-entry.jsonwithwritePathReceipt: mode 0600, replaced with one rename, and only when something was applied, so an earlier receipt stays. When it cannot be written, the entry is taken back and the step fails. The file holdsschema: 1,kindand that kind's fields:pathandtargetfor a link;path,line,createandseparatorfor a profile line;key,nameandentryfor the userPath. -
Reading it back.
readPathReceipt(layout, { env, home })accepts only what this prefix writes:~/.local/bin/gentle-shelllinked to our launcher; our exact marked line for ourbinin a startup filepathEntryPlancan choose for this user (profileFiles:.zshrcin$ZDOTDIRor home, the macOS bash login files or Linux~/.bashrc, the fishconf.dfile under$XDG_CONFIG_HOMEor~/.config,~/.profile), with the fish line only in the fish file; or ourbininHKCU\EnvironmentPath. Anything else, a link or a malformed file isinvalidand is never acted on: a receipt naming another file, even one that holds our exact line, is named for manual removal. -
Safe appends. The line goes in with
O_NOFOLLOW. A profile that turned into a link after planning is refused. -
Registry. The registry change goes through an injected adapter.
-
No
pnpm setup. It is not used.
bin/gentle-shell.mjs runs in bundled mode only when it runs from a bundled install. Its real package root must be versions/<id>/node_modules/gentle-pi of a version whose .gentle-shell-version marker reads <id>, and current must name that version. resolveBundledInstall in lib/gentle-shell-bundled.ts decides this. Any other installation keeps the launcher's ordinary behavior, including a kept version that current does not name.
-
Exact Pi (S8). The launcher runs only the Pi that the version's
package.jsonpins:versions/<id>/node_modules/@earendil-works/pi-coding-agent, inside the version folder, with that exact version and Pi'sdist/bundle/cli.jsbin.GENTLE_SHELL_PI, an adjacent fallback andPATHare never used. A missing or different Pi stops with a message that names the version and the pinned Pi, and says to rungentle-shell upgradeor the installer again. -
Child environment (S9). Pi's bash tool passes its own environment to every command the agent runs in the user's projects. Pi and its subagents therefore get the user's environment unchanged:
PATH,NODE_OPTIONSandnpm_config_*stay the user's, sogit,gh,nodeandnpmthere are the user's own.bundledChildEnvadds only Gentle Shell's own variables:GENTLE_SHELL_PIis removed.GENTLE_PI_AGENTS_PIis set to our node and the bundledcli.js, so subagents never run apifromPATH. The subagent runner splits that variable on whitespace, so a path with a space leaves it unset. The runner then falls back to the running node andcli.js, which are the same pair.PI_SKIP_VERSION_CHECK=1.GENTLE_SHELL_PREFIXandENGRAM_BIN(Isolation from the user's Gentle AI).- The launcher still sets the agent home variables (
PI_CODING_AGENT_DIR,GENTLE_PI_AGENT_HOME) as always.
Pi itself needs no
PATHentry, because it is started as our node with the bundledcli.js. -
Pi's own npm. Pi runs npm through its
npmCommandsetting (getNpmCommandin Pi'score/package-manager.js). For a home Gentle Shell owns, the launcher sets that key in itssettings.jsontobundledNpmCommand(layout): our node, our npm entryruntime/node-<v>/gentle-shell/npm(npm for Pi),--userconfig=<prefix npmrc>,--globalconfigand--cacheinside~/.gentle-shell/npm/, and--update-notifier=false. No other part's basename isnpm,pnpmorbun, which Pi would reject as ambiguous. "Owns" means the isolated home, or a--homeit created or provisioned. It never sets the key for--link, a foreign home, or the user's~/.pi.- Only that key is written, and only when it differs. A file that already holds the command keeps its bytes, and one that is not a JSON object is left alone with a warning. An earlier value (
npm-cli.jsdirectly, or a--prefixflag) is replaced the same way. - npm ranks command-line flags above
npm_config_*variables, so the user's environment cannot redirect these three settings. Annpm_config_*key that has no flag here (for examplenpm_config_registry) still reaches our npm, as the user's own network setting would. - No
--prefix. A--prefixflag on the command line also sets npm's local prefix (loadLocalPrefixin@npmcli/config). Pi installs a git extension's dependencies withnpm installin the clone and passes no--prefixof its own, so that install would land in the prefix and fail. Pi'snpm:installs and removals pass--prefix <agent home>/npmthemselves. - The only global read is
npm root -g(Pi's lookup of packages an older Pi installed globally). It then resolves to the user'snpm_config_prefixor our node folder'slib/node_modules. It only prints a path and creates nothing.
- Only that key is written, and only when it differs. A file that already holds the command keeps its bytes, and one that is not a JSON object is left alone with a warning. An earlier value (
-
Setup.
gentle-shell setupruns Gentle AI, which findspionPATH. pnpm's.bin/pishim runs the firstnodeonPATH. So setup alone gets our node's folder, then the version'snode_modules/.bin, ahead of the user'sPATH(bundledSetupEnv). It also gets Gentle AI's isolation variables. Everything else in its environment is the same as Pi's. -
Pi updates (S5).
gentle-shell updatewith no target,--self,selforpi,--all, orself/piwith--extensionsselects the newest published stable Pi for the current Shell.--latestis an alias. It warns before installing without the release's verified lockfile, verifies the new pair, and keeps the old pair for rollback. An "all" form still sendsupdate --extensionsto Pi if the Pi update fails, and exits nonzero for the failure. Explicit extension pins stay pinned. Package targets,--models, help, and forms Pi refuses go to Pi unchanged;--forceon a managed self update is rejected. -
What cannot be intercepted. A
pi updatethat someone runs directly with the bundledpi(for example the agent's shell, which finds it throughnode_modules/.bin) is Pi's own command. In Pi 1.1.0,PI_SKIP_VERSION_CHECKhides only the startup "Update Available" notice (utils/version-check.js). For the update itself,package-manager-cli.jsdetects a pnpm install from the.pnpmpath, andconfig.js(getSelfUpdateCommand) finds no global pnpm root that ownsversions/<id>. Pi then printscannot self-update this installationand changes nothing.
A bundled Shell shares the global Engram server, like other agents' hooks: it reuses a verified server already listening on the default endpoint and, when nothing listens, starts one itself with its verified bundled core. It decides before setup and again right before Pi starts. When memory may not be used, Shell keeps working without memory and points ENGRAM_BIN at its own guard, so no provider, whichever Pi route loaded it, can open, create or migrate a store directly. The core pin is 3.5.2, acquired through the existing size- and checksum-verified path.
Evidence (default ~/.engram, default endpoint 127.0.0.1:7437) |
Memory | ENGRAM_BIN / ENGRAM_URL |
|---|---|---|
Running server answers the exact GET /compatibility ready contract |
Allowed; reuse that server unchanged | Guard + ENGRAM_URL=http://127.0.0.1:7437 |
| Nothing listening, verified bundled core | Allowed once the server Shell starts (below) answers ready; otherwise disabled for that launch | Guard + ENGRAM_URL=http://127.0.0.1:7437 |
Nothing listening, but engram.db's header user_version is newer than the bundled core's schema |
Disabled; no server is started | Guard |
| Nothing listening, but the bundled core is missing or unverified | Disabled | Guard (never an engram from PATH) |
| Anything else listening: a different service, timeout, 401/404, an older server, malformed answers | Disabled; the listener is never stopped or replaced | Guard |
Explicit ENGRAM_BIN, ENGRAM_URL, ENGRAM_PORT, ENGRAM_SOCKET, ENGRAM_DATA_DIR, ENGRAM_DATABASE_URL, ENGRAM_SERVER_MODE, or an invalid ENGRAM_STARTUP_POLICY |
Disabled, never probed or started | Explicit ENGRAM_BIN/ENGRAM_URL kept; ENGRAM_URL alone gets the verified core (or the guard) as ENGRAM_BIN; otherwise guard |
An existing store at ~/.engram does not disable memory by itself, whoever created it (an earlier Shell or the global Engram CLI): the server owns the store, and Shell only talks to a server that proves itself through /compatibility. The one exception is the header tripwire: when engram.db is a regular, unlinked file whose first 100 bytes are a valid SQLite header (magic, page size, bytes 18/19 each 1 or 2) and whose user_version (bytes 60-63) is greater than the bundled core's schema (1, verified from v3.5.2:internal/store/store.go), a newer Engram migrated the store, so Shell does not start its older core on it. Start that newer Engram's server yourself; Shell then reuses it. A pin without a verified schema entry treats any readable header as newer. Equal or lower (including a stale 0 before a WAL checkpoint) is no signal either way.
Classification never runs an Engram executable, never opens SQLite, and never infers compatibility from a version string. It reads the raw database header and makes bounded (1 s) read-only GET /compatibility and GET /health requests to the default endpoint on a direct loopback socket, without proxy, redirects or credentials. gentle-shell --version and a Pi version or help flag (--version, -v, --help, -h where Pi reads it as a flag: before Pi's own -- and not as another option's value, so a prompt or option value containing -v does not count) never probe, start a server, ensure the companion or print memory notices. A Pi subcommand (gentle-shell list, install, ...) and gentle-shell setup never start a server either.
Only the final pre-Pi decision starts a server. Shell runs the verified bundled core as plain engram serve, with Engram's own default port and data directory, detached (its own process group, stdio ignored, unreferenced, windowsHide on Windows, working directory the home folder), so it outlives Pi like a server other agents' hooks start. Its environment is the user's minus every ENGRAM_* setting and GH_TOKEN/GITHUB_TOKEN, plus ENGRAM_NO_UPDATE_CHECK=1. For TMPDIR/TMP/TEMP whose final path component is session scratch (gp or ga followed by exactly six alphanumeric characters), POSIX drops the setting. Windows instead explicitly replaces it with its original absolute ancestor outside consecutive scratch components: omitting TEMP can cause Node to inherit the parent's scratch again. When any Windows scratch setting is replaced and TEMP is absent, Shell also sets TEMP to that stable ancestor. Existing custom temp settings retain their own values, even when they name different roots. Windows path separators and case-insensitive environment keys are supported. An unsafe relative scratch path disables this startup attempt before spawning; Shell never guesses a root or creates a temp folder. Unrelated variables remain unchanged. Pi keeps the user's environment. Shell then polls GET /compatibility for up to 10 seconds. A ready answer allows memory through the reuse URL; otherwise (the core fails to start, exits, or never answers ready) memory is disabled for that launch with a notice, and Shell still starts Pi. Concurrent launches converge: when the child exits (for example because another launch won the bind), Shell keeps polling for a short grace, and a ready /compatibility from the winner counts as started. Shell never stops a server it started or found; it keeps running for every client until you stop it.
bin/engram-guard is a POSIX sh script, run directly (no shell needed by the caller). It ignores its arguments, prints ENGRAM_STARTUP_NOTICE (as data, never executed) to stderr and exits 78. It never runs Engram and never reads or writes Engram data. gentle-engram 0.4.1 reaches local data only by running ENGRAM_BIN (instance-id first, before any health probe or spawn), so the guard stops it before any store access; 0.4.2 also honors ENGRAM_STARTUP_POLICY=disabled, and with ENGRAM_URL it talks to the shared server over HTTP. Pi, its subagents and the agent's shell all inherit the same environment.
- Explicit selections bypass the guard. An
ENGRAM_BINorENGRAM_URLyou set is never replaced or removed (with onlyENGRAM_URL,ENGRAM_BINis still the verified core, or the guard without one, never aPATHengram); memory is still marked disabled through the policy, which only 0.4.2+ honors. AnENGRAM_SERVER_MODEyou set is passed through untouched; Shell never selects one. - Windows. The guard has no
.exe; spawning it fails (for exampleENOENTorEINVAL), which still fails closed, but no notice is printed.
GET /compatibility (published in Engram 3.4.0 through PR #1792; 3.3.2 answers 404) must return status: "ready", active_reuse_authorized: true, capability: "startup-verified-store-v1", revision: 1, source: "active", 32-hex instance_id and runtime_id, and no offline_automatic_opening_authorized: true. Engram 3.5.2's plain serve answers exactly this. The endpoint uses the server's requireAuth: open when the server has no ENGRAM_HTTP_TOKEN, otherwise it needs Authorization: Bearer <token>. Shell owns no such secret and sends none, so a token-protected server answers 401 and stays unknown (disabled). The server Shell starts never inherits ENGRAM_HTTP_TOKEN.
When the running server's /health reports a valid 32-hex instance_id and a canonical numeric x.y.z version (optional v prefix, no leading zeros, at most four digits per component) older than 3.4.0, and /compatibility does not answer ready, the notice names that version and asks to back up the data, update that server to 3.4.0 or later with its own installation method, and restart it. The version only selects this wording: memory stays disabled, and any other version string is never shown. Otherwise, when disabled, Shell explains that it could not verify safe server reuse. Matching versions or updating alone do not prove compatibility. Back up existing data before manual changes; do not delete the store or bypass the safety check. Shell rechecks on every launch and remains usable without memory. Ordinary startup performs no migration, deletion, or process termination.
Engram 3.3.2 does not expose /compatibility, so reinstalling that version cannot authorize reuse of an existing global store. Engram 3.4.0 includes the endpoint from PR #1792. Back up the existing store before updating its server through its own installation method, then restart that server and Shell. Updating Shell's bundled binary does not replace an already-running global server. Reuse still requires a valid ready response, not just the version number; a token-protected server cannot provide it to Shell's unauthenticated probe. Do not disable authentication to bypass this policy or use an unpublished build.
The memory decision never skips or fails setup: gentle-shell setup publishes the verified core and runs Gentle AI. Gentle AI 4.0.0 install --agent pi runs no engram setup for Pi, and pi-engram init only edits settings, so neither opens a store. Setup reclassifies after publishing the core, and the decision only selects the Engram environment its children and the launched Pi receive. On a new machine the wizard publishes the runtime without Engram, so the first gentle-shell setup publishes the core and the following launch starts the shared server.
In a Shell-owned home, a missing companion is installed through Pi as npm:gentle-engram and checked for engram.startupPolicy: 1; a failed check is reported and Shell still starts, protected by the guard. Shell migrates its old npm:gentle-engram@0.4.2 registration to the unversioned source using Pi's package manager without installing an update. Pi then includes it in its normal startup package-update notice; run gentle-shell update --extensions to update it. An existing selection is not automatically reinstalled on launch, and linked/foreign homes, local companions, other companion pins and unrelated extension pins are not rewritten. This changes the Pi package, not the separately pinned native Engram core. setup --dry-run installs nothing.
Earlier Shell releases provisioned ~/.engram themselves and recorded <prefix>/config/engram-provenance.json; ordinary startup no longer reads that record. To migrate such a Shell-provisioned store with the bundled core explicitly, stop every Engram client and server using the directory, retain a complete backup (including WAL and configuration), then run:
gentle-shell memory migrate --confirm-quiescentRelease dependency: this requires a published bundled core advertising migrate-copy. The pinned Engram 3.5.2 implements it; Engram 3.4.0 did not. Shell checks help in an isolated home and refuses without invoking an unknown migration command or changing the source/provenance. Do not substitute an unpublished binary, edit hashes, or assume equal schema versions authorize recovery.
When supported, the provider copies the complete directory to a new sibling and validates/migrates only that copy. Shell checks the provider's directory and identity evidence, renames the original to the reported backup_dir, promotes the copy, and atomically replaces its core-bound provenance. Success prints one JSON record with status: "migrated", instance_id, and backup_dir. Keep that backup until you have verified your records in Shell. Provider failures preserve the original and provenance; any incomplete copy is identified and retained, never promoted. Explicit foreign data locations or endpoints are not adopted by this command.
The confirmation is an operator assertion, not a process lock: source hashes detect observed changes but do not establish that every writer has stopped. Never run recovery while another integration is using the store.
A marker at <prefix>/config/engram-migration.json blocks memory before probes or starting a server. It records the source, output, backup paths and originalProvenance. Keep clients stopped and do not simply remove this marker. Preserve all paths while inspecting them.
To roll back an interrupted promotion, first verify the paths and recorded instance identity. If backup exists, it is the original directory: retain any current source under a separate quarantine name, then restore backup to source. If no backup exists, do not replace the original source with the incomplete output. Restore originalProvenance atomically to <prefix>/config/engram-provenance.json, preserving the old core hash exactly. Only after source identity and provenance are reconciled may the marker be removed. Retain the migration output for diagnosis. If these facts cannot be established, leave the marker and request recovery assistance rather than guessing.
No automatic retry, store deletion, or server termination is performed.
- Shared-store residuals. The header tripwire only sees a newer schema recorded in
engram.db's header. A different Engram that changed the store without raisinguser_version, or changes still only in the WAL, are not detected without reading the database; the server that opens the store owns its migrations. - A detached server keeps running. The server Shell starts is not tied to Pi's lifetime, by design (it is shared). Stop it like any Engram server when you no longer want it; Shell starts it again on the next launch if nothing listens.
- Older or other integrations. gentle-engram versions before 0.4.1, and integrations that do not reach data through
ENGRAM_BIN(for example an MCP launcher runningengramfromPATH), are not covered by the guard. - Non-bundled installs keep their previous behavior.
S9. Everything Gentle AI and gentle-pi keep lives in the prefix, except Engram's memory data.
| Process | Gets |
|---|---|
gentle-shell setup (the pinned gentle-ai install --agent pi) |
GENTLE_AI_HOME=<prefix>/gentle-ai, GENTLE_AI_PI_CONFIG_DIR=<prefix>/config/gentle-ai, GENTLE_AI_ENGRAM_BIN=<prefix>/runtime/engram-<v>/engram, GENTLE_AI_NO_SELF_UPDATE=1 and GENTLE_PI_CONFIG_HOME=<prefix>/config. |
| Pi and its subagents | GENTLE_SHELL_PREFIX=<prefix> and the memory startup policy; ENGRAM_BIN is the Shell guard, plus ENGRAM_URL=http://127.0.0.1:7437 when the shared server is verified. An explicit ENGRAM_BIN or ENGRAM_URL is kept as is (Engram startup safety). No GENTLE_AI_* variable, and GENTLE_PI_CONFIG_HOME is left as the user has it. |
gentle-pi's own gentle-ai runs (review, telemetry trigger, runtime metrics) |
The same five variables as setup. |
| The agent's shell | Pi's environment: the user's own plus the Pi row above. |
- Running bundled.
runningBundledPrefix(lib/gentle-shell-bundled.ts) decides it for every rule below.GENTLE_SHELL_PREFIXmust be absolute and name the root of the bundled install that this gentle-pi runs from.resolveBundledInstallmust find a finished version of that prefix around the package (its version marker).currentneed not name it: a subagent started aftergentle-shell upgrademovedcurrentruns from the previous, kept version and stays bundled. The launcher's own choice of version still requirescurrent. A gentle-pi the user installed elsewhere (npm, pnpm, nvm) and started from the agent's shell, which inherits the variable, is not bundled. Outside bundled mode every rule here is off, and everything behaves as in a pnpm or npm install. The answer is remembered per package root and prefix value. So agentle-shell upgradethat movescurrentwhile Pi runs does not change the running Pi's config home mid-session. - gentle-pi's config home. In bundled mode,
gentlePiConfigHome(lib/agent-home.ts) is<prefix>/config/gentle-ai. Three other readers use the same rule: the profile reader and pin resolution inextensions/gentle-shell.ts, the banner settings inextensions/startup-banner.ts, and the dev-binary registration inlib/gentle-ai-binary.ts. It is derived, never exported to Pi: an exported value would redirect agentle-aithe user runs in the agent's shell. Outside bundled mode it staysGENTLE_PI_CONFIG_HOME, else~/.pi/gentle-ai. The bundled config home starts empty: profiles and preferences in~/.pi/gentle-aiare not copied.- In bundled mode a
GENTLE_PI_CONFIG_HOMEthe user set is ignored by gentle-pi: the bundled config home wins. The variable still reaches the agent's shell unchanged, so the user's owngentle-aior gentle-pi runs there are unaffected. - Aligned with Gentle AI 4.0.0. Gentle AI 4.0.0 reads
GENTLE_PI_CONFIG_HOMEas the parent of its owngentle-aifolder:piBackgroundPolicyPathininternal/cli/pi_background.gowrites<GENTLE_PI_CONFIG_HOME>/gentle-ai/background-subagents.json, else~/.pi/gentle-ai/.... So the config home is<prefix>/config/gentle-ai, and only the Gentle AI processes Gentle Shell starts also getGENTLE_PI_CONFIG_HOME=<prefix>/config. The policy 4.0.0 writes during setup then lands in the bundled config home, and a release with gentle-ai#5462 writes the same folder throughGENTLE_AI_PI_CONFIG_DIR. In 4.0.0 onlyinstall,syncand their dry runs read it (the background-policy projection); review and telemetry runs do not, and get it only so every Gentle AI process Gentle Shell starts agrees.
- In bundled mode a
- Gentle AI's variables never reach Pi. Pi's bash tool would pass them to a
gentle-aithe user runs in the agent's shell, and that run would then use the prefix. Instead,gentleAiIsolationEnv(lib/gentle-ai-binary.ts) adds them to eachgentle-aithat a bundled gentle-pi starts itself (the centralgentleAiProcessEnvironment, the review relay, the runtime metrics send and the telemetry trigger), and the launcher adds them to setup.GENTLE_AI_ENGRAM_BINis set only when our Engram is published. - Engram.
ensureEngram(scripts/bundled-install.mjs) publishes the pinned Engram release underruntime/engram-<v>/. The archive is checked against its pinned size and sha256 from the release'schecksums.txt. Only theengram(orengram.exe) regular file is written.engram versionmust printengram <v>from the staging folder before the folder is published.ensureRuntime({ engram: true })includes it.gentle-shell setupin a bundled install ensures it before it runs Gentle AI, and stops if it cannot, whatever the memory decision (Engram startup safety).gentle-shell upgradeand--rollbackpassengram: trueto the release's ownensureRuntime. WithGENTLE_AI_ENGRAM_BIN, Gentle AI uses that binary as is: no brew, download,go install,PATHor registry change, and nogoorbrewdependency probes. - gentle-engram. The gentle-engram Pi extension finds Engram only through
ENGRAM_BIN, read once when it loads, and otherwise runsengramfromPATH. So Pi getsENGRAM_BIN(the guard; memory reaches the shared server throughENGRAM_URL), and noPATHentry is added. AnENGRAM_BINthe user set is kept. The agent's shell inheritsENGRAM_BINtoo, so an Engram MCP launcher started there throughENGRAM_BINuses the same guard. - Personas. Setup snapshots and restores the persona Gentle AI may rewrite: the prefix's
config/gentle-ai/persona.json, and also the user's~/.pi/gentle-ai/persona.json. A Gentle AI release without the variables still writes the user's file, and restoring an unchanged file does nothing. The managed-asset record is restored only in the user's~/.gentle-ai/state.json; the prefix's record belongs to the pinned Gentle AI. - pi-pretty and prompt history. In bundled mode only, the pi-pretty wrapper passes
deps.sdkwith Pi's five tool factories andgetAgentDir, so its FFF database lands in<agent home>/pi-pretty/fff, and prompt history keeps its store under<agent home>/history(Prompt history). Elsewhere both keep their existing locations: Pi's deps reach pi-pretty unchanged ($HOME/.pi/agent/pi-pretty/fff, or no store withoutHOME), and history stays under~/.pi/agent/history, also in the isolated home of a pnpm or npm install, so existing histories are not left behind. - Effective with a later Gentle AI. gentle-pi pins Gentle AI 4.4.0 today (
INSTALLER_VERSION); the following are historical compatibility notes for Gentle AI 4.0.0, which ignores the fourGENTLE_AI_*variables. Whether 4.4.0 includes gentle-ai#5462 is unverified here. Until gentle-pi pins a Gentle AI release that contains gentle-ai#5462:- Setup still writes
~/.gentle-aiand~/.pi/gentle-ai, with the restores above, and still acquires Engram its own way. - The persona goes to the user's
~/.pi/gentle-ai/persona.json(Gentle AI 4.0.0 resolves it from the OS home) and is restored there. The prefix'sconfig/gentle-ai/persona.jsonis not created by setup. - The background-subagents policy already lands in
<prefix>/config/gentle-ai/throughGENTLE_PI_CONFIG_HOME=<prefix>/config(see above).
- Setup still writes
What stays the user's:
git,ghandcodegraph, which the agent runs for the user's work, from the user'sPATH.- Engram's memory data in
~/.engram, shared with the user's other agents. - pi-pretty's reads of
$HOME/.pi/agent/settings.json(itsindex.jsandrender.jslook for its settings there whatever the agent home is). This needs an upstream pi-pretty change;node_modulesis not patched. GENTLE_SHELL_PREFIXandENGRAM_BINreach the agent's shell, like the agent home variables. A gentle-pi the user runs there is not bundled and ignores the prefix (see above).
Opening a bundled gentle-shell in a terminal checks the npm gentle-pi/latest release before Pi starts. The check waits at most 1.5 seconds; offline (PI_OFFLINE or --offline), unavailable, malformed, same or older releases open normally. Plain startup and --continue/--resume are supported; commands, other forwarded options, noninteractive/RPC launches, and subagents do not check or prompt. Global npm/pnpm installations are unchanged.
A rose-colored panel shows the current and available Shell versions and a link to that release’s notes. Use arrows or 1–3 to select, then Enter to confirm:
- Update now (gentle-shell upgrade) uses the existing verified bundled upgrade. On success, it automatically reopens the newly active Shell with that release’s Node and Pi and the original startup arguments, without showing the prompt again on the handoff. If upgrade fails, its error is shown and the current Shell opens normally; a failed relaunch reports an error rather than silently running the old version.
- Not now (the default; also Escape or end of input) opens normally without saving a preference.
- Skip this version stores only the offered version as
skippedStartupReleasein the launcher’sconfig.json(orGENTLE_SHELL_CONFIGoverride), preserving unrelated settings. Another newer release can prompt again. A malformed or unwritable file is not replaced; the launcher warns and continues without saving.
Ctrl+C cancels opening (exit 130). No update runs without explicit confirmation. The startup check does not update additional extensions; use gentle-shell update --extensions for those. Restarting does not change the working directory, startup arguments, or agent home. A newer independently updated Pi is kept across Shell releases.
upgradeBundled follows the release channel only. --channel main is refused until a later task.
- The latest release comes from the npm registry, as on the ordinary release channel (
latestRelease). Its two assets come fromfetchDistribution(identity encoding, size bounds), andreadDistributionchecks the exact versions, the workspace files and the lockfile sha256. A release without assets stops with its version, and nothing changes. - When that release is the active version, the command says it is already current and changes nothing.
- Otherwise
ensureRuntimeruns (with Go on Windows), andinstallVersioninstalls the release in place beside the active version. If the active Pi is newer than the release's verified Pi, Shell keeps that Pi and warns that it installs without the release's lockfile. The release lockfile is used only for its exact pair. - The new version's own
scripts/bundled-install.mjsis loaded, only from that freshly installed folder, and only when its real path stays inside it. ItsprefixLayout,ensureRuntimeandensureLaunchermust all be functions; otherwise the upgrade stops. ItsensureRuntimeinstalls that release's pinned Node and pnpm beside ours when the pins moved, and reuses them otherwise. - The new version's launcher must print exactly
gentle-shell <shell>andpi <pi>. It runs with the release's Node, in the pnpm environment, with noGENTLE_*orPI_*overrides. It runs in bundled mode for that version, with its exact Pi: the only extra variable,GENTLE_SHELL_VERIFY_VERSION=<id>, makesresolveBundledInstalltreat that id as current. The launcher honors it for--versiononly, and never passes it to a child. ApionPATHcannot pass this check. - Only then does
currentswitch (activateVersion, one rename), and the release's ownensureLauncherwrites the launcher for its Node. If the launcher cannot be written,currentswitches back and the command fails. pruneVersions(layout, 2)keeps the new version and the previous one. If pruning fails, the upgrade still succeeds and prints a warning.
Any failure before step 7 leaves current as it was and removes a version folder this command installed.
gentle-shell upgrade --rollback switches current to the most recently active other version that is still installed. It uses that version's own scripts/bundled-install.mjs, loaded and checked as in step 4. Its ensureRuntime makes sure its pinned runtime is present, and its ensureLauncher writes the launcher for its Node. Without a usable module, nothing changes. If the launcher cannot be written, current switches back. Running --rollback twice returns to the first version.
Known limits: a release whose lockfile needs a newer pnpm than ours cannot be installed by this upgrade, because the release's own runtime is installed only after its version folder exists. Older runtimes under runtime/ are never removed.
An existing Gentle Shell that a global pnpm or npm owns moves to the bundled install (S4). migrationGate in scripts/bundled-wizard.mjs decides, as a pure function. All of these must hold:
- The channel is
release, and the target is supported. installOwnernames the owner:pnpmornpm. Annpm linkor an unknown owner is never moved.gentle-shell upgradenames it and keeps its current path.- The release publishes both distribution assets, and that release is not older than the installed Gentle Shell.
Otherwise both entries keep their current path, unchanged.
The prefix folder. The launcher creates the isolated home ~/.gentle-shell/agent (and ~/.gentle-shell) with mode 0700, whatever the umask. An existing ~/.gentle-shell that is a real folder of this user but group- or other-writable (umask 002 before this fix) loses that write access (go-w, through a descriptor that does not follow links) when the prefix is claimed. A link, a non-folder or another owner's folder is still refused.
gentle-shell upgrade (migrateGlobalShell). runUpgrade offers the migration on the release channel only, through its migrate adapter, before its own update. The owner probe (ownerManager, which runs the user's npm root -g) runs only after the assets read as published and the versions allow a migration; otherwise nothing is probed.
- A few lines explain the move: the prefix, the exact Pi, the user's Node, npm, pnpm and Pi untouched, and
~/.gentle-shell/agentstaying where it is. runBundledInstallruns the bundled steps with a plan whose removal isask: claim, npm settings, runtime, version, activate, launcher, thenverify-launcher(the launcher's own--version,checkLauncher), then the PATH entry, so a launcher that fails its check is never put on PATH. There is no setup step: the launcher's automatic setup runs on first use.- Then one question:
Remove the old global gentle-pi with <manager> (<command>)? [y/N]. End of input or Ctrl+C answers No. Without a terminal, nothing is removed and the exact command is printed. - A yes runs
pnpm remove -g gentle-piornpm uninstall -g gentle-pi, throughownerManager's verified invocation (removeGlobalShell).
A failed plan or step (including the launcher check) prints the step and its detail, which is the error message with the home shown as ~, and says the global gentle-pi was left untouched. The upgrade then continues with the global package's own update for this run, and exits with that path's code. A failed removal keeps the bundled install, names the manual command and exits 1. After a successful install, the release channel is recorded and a main override this module made is removed.
When the old package stays, shadowingCommand predicts which gentle-shell a new terminal runs. It takes this PATH with the planned change applied. If the old one still wins, the command names it and says how to change that. The order it assumes per startup file:
| Planned change | Position | Why |
|---|---|---|
zsh line in $ZDOTDIR/.zshrc |
first | zsh reads .zshrc after .zprofile (and macOS path_helper in /etc/zprofile); our line is appended after pnpm setup's block. Only ~/.zlogin, read later, could prepend again. |
bash line in the macOS login file, or ~/.bashrc |
first | Appended last to the one file bash reads for terminals; pnpm setup writes earlier. A Linux ~/.profile that sources .bashrc and then prepends ~/bin or ~/.local/bin could precede it. |
fish line in conf.d/gentle-shell.fish |
last | fish reads conf.d before config.fish, so whatever config.fish prepends (pnpm setup's $PNPM_HOME/bin) comes first. The current PATH already shows those entries, so the bundled bin counts after all of them. This may report a shadow for an entry that config.fish did not add. |
| Windows user Path entry | last | Appended to the user Path, after the machine Path. |
~/.local/bin link, or no change |
where it already is |
The web installer. An existing Gentle Shell that pnpm or npm owns gets the bundled plan with removal plan. In that plan, verify-launcher runs before bundled-path, and a final remove-global-shell action follows. A failed step keeps the failed outcome and carries its detail (the error message; for verify-launcher, the versions the launcher reported against the expected ones), which the outcome screen shows. Consenting to the plan is the one confirmation. The removal runs only after every step passed. It finds the installed Gentle Shell again, and runs only when the same manager still owns it. A failed removal keeps the outcome and adds removeGlobal { status: "failed", command }; the outcome screen shows that command.
Only the PATH entry and the old global gentle-pi change outside the prefix. The user's Pi and ~/.gentle-shell/agent are never touched.
gentle-shell self-uninstall [--dry-run] [--yes] [--include-shared], run from the bundled launcher, removes the bundled install (S6, S7). It keeps the plan, the confirmation, --dry-run, --yes, end of input (cancel) and Ctrl-C (abort) of the global-install path, which is unchanged.
- Ownership. The prefix must be the real folder the launcher runs from, never reached through a link. On POSIX it must be the real
~/.gentle-shell; on Windows it must carry the.gentle-shell-bundlemarker with its exact text. Otherwise nothing is removed. No removal may be, contain or lie inside the home folder,~/.pi, the user's Pi home,~/.gentle-aior~/.engram. - PATH first. The entry the receipt records is removed first: only our link, our exact line or our
Pathentry. A failure stops before anything else is removed. A line the user edited, or a link that no longer points at our launcher, is kept, and the last message names the file and the marked line it still holds. Without a valid receipt the plan names the entry to remove by hand, once, and nothing is guessed. - Removed.
bin/,current(the link itself),versions/,versions.history,runtime/,pnpm/,npm/,gentle-ai/,config/(Gentle Shell's settings,config/gentle-ai),tmp/,path-entry.jsonand, on Windows, the marker. Each is checked withlstat; a link in their place or anything else is kept and named, and no link inside them is followed. - Shared. The Gentle Shell home (
~/.gentle-shell/agent) and the launcher'sconfig.jsonare kept unless the user accepts a separate question (default No) or passes--include-shared. - The prefix. It is removed only when nothing else is left in it, and only when it held nothing the install did not write.
- Windows. The running
node.exe, the.cmdlauncher and the marker cannot be removed by this process. Everything else goes at once, thePathentry included. A hidden Windows PowerShell (scheduleWindowsRemoval; a detachedcmd.exestarts it withstart, so it outlives the launcher) waits for this process to exit, deletesnode.exeand the launcher, then the marker only when nothing is left at their paths (so a failed removal can be retried withself-uninstall), then each folder above them that is empty, and the prefix when it may go. It never removes recursively. The paths reach it as environment data, never as script text.
- The user's Node, npm, pnpm, Go,
PNPM_HOME,~/.npmrcand npm or pnpm global configuration. - The user's Engram binary and Homebrew. Gentle AI's
~/.gentle-aiand~/.pi/gentle-aionce gentle-pi pins a Gentle AI release with gentle-ai#5462 (Isolation from the user's Gentle AI). - Anything outside the prefix, except the single PATH change and, after a migration the user confirmed, the old global gentle-pi removed by its own manager.
- Folders, files, launchers or
currententries without our marker.