gentle-pi 4.2.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (334) hide show
  1. package/README.md +340 -138
  2. package/assets/agents/gentle-ai-explore.md +1 -0
  3. package/assets/agents/gentle-ai-verify.md +6 -4
  4. package/assets/agents/gentle-ai-worker.md +7 -1
  5. package/assets/design-canvas/canvas.css +648 -0
  6. package/assets/design-canvas/canvas.js +1497 -0
  7. package/assets/design-canvas/index.html +88 -0
  8. package/assets/design-canvas/inspector.js +290 -0
  9. package/assets/orchestrator-delegation-reference.md +163 -0
  10. package/assets/orchestrator-delegation.md +25 -176
  11. package/assets/orchestrator-tracking.md +1 -1
  12. package/assets/orchestrator-verification.md +14 -8
  13. package/assets/orchestrator-writer.md +5 -1
  14. package/assets/orchestrator.md +11 -11
  15. package/bin/engram-guard +1 -1
  16. package/bin/gentle-shell.mjs +237 -139
  17. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  18. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
  19. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
  20. package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
  21. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
  22. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
  23. package/docs/assets/diagrams/agent-orchestration.svg +54 -15
  24. package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
  25. package/docs/assets/diagrams/measured-routing.svg +49 -0
  26. package/docs/assets/diagrams/native-review.svg +52 -12
  27. package/docs/assets/diagrams/odd-workflow.svg +128 -66
  28. package/docs/assets/diagrams/permission-modes.svg +51 -0
  29. package/docs/assets/features/audio-notifications.png +0 -0
  30. package/docs/assets/features/command-palette.png +0 -0
  31. package/docs/bundled-install.md +65 -30
  32. package/docs/delegated-verification.md +16 -8
  33. package/docs/gentle-agents-activity.md +16 -6
  34. package/docs/gentle-music.md +61 -0
  35. package/docs/gentle-shell-uninstall.md +8 -1
  36. package/docs/gentle-shell.md +80 -9
  37. package/docs/install-wizard.md +37 -6
  38. package/docs/permission-modes.md +8 -3
  39. package/docs/readme-reference.md +120 -28
  40. package/docs/session-profile-format.md +26 -0
  41. package/docs/voice.md +259 -0
  42. package/docs/yolo-mode.md +38 -10
  43. package/extensions/ask-user-choice.ts +4 -1
  44. package/extensions/codegraph-tools.ts +68 -6
  45. package/extensions/gentle-agents.ts +270 -48
  46. package/extensions/gentle-ai.ts +490 -113
  47. package/extensions/gentle-design.ts +158 -0
  48. package/extensions/gentle-herdr-activity.ts +2 -3
  49. package/extensions/gentle-music.ts +289 -0
  50. package/extensions/gentle-shell.ts +359 -85
  51. package/extensions/gentle-todo.ts +43 -9
  52. package/extensions/gentle-voice.ts +11 -0
  53. package/extensions/quiet-tools.ts +1 -1
  54. package/extensions/session-scratch.ts +384 -0
  55. package/extensions/skill-registry.ts +63 -8
  56. package/extensions/startup-banner.ts +2 -3
  57. package/lib/agent-home.ts +8 -3
  58. package/lib/agent-profiles.ts +26 -0
  59. package/lib/agents-config.ts +35 -0
  60. package/lib/agents-protocol.ts +60 -1
  61. package/lib/agents-runner.ts +107 -9
  62. package/lib/agents-session-transport.ts +101 -6
  63. package/lib/agents-view.ts +3 -1
  64. package/lib/agents-widget.ts +14 -1
  65. package/lib/auto-permission-memory.ts +48 -18
  66. package/lib/auto-permission-runtime.ts +21 -6
  67. package/lib/bounded-writer-admission.ts +51 -0
  68. package/lib/clipboard-image.ts +147 -0
  69. package/lib/command-palette-catalog.ts +2 -4
  70. package/lib/commit-slice-signal.ts +100 -0
  71. package/lib/context-backstop.ts +120 -0
  72. package/lib/delivered-test-lint.ts +80 -0
  73. package/lib/design-canvas-server.ts +947 -0
  74. package/lib/design-comments.ts +437 -0
  75. package/lib/design-handoffs.ts +313 -0
  76. package/lib/design-open-browser.ts +58 -0
  77. package/lib/design-sources.ts +120 -0
  78. package/lib/destructive-command-guard.ts +120 -9
  79. package/lib/external-editor.ts +31 -0
  80. package/lib/gauge-theme.ts +146 -0
  81. package/lib/gentle-ai-binary.ts +28 -7
  82. package/lib/gentle-design.ts +513 -0
  83. package/lib/gentle-shell-bundled.ts +146 -36
  84. package/lib/gentle-shell-import-pi.ts +233 -0
  85. package/lib/gentle-shell-launcher.ts +154 -15
  86. package/lib/gentle-shell-startup-update.ts +61 -9
  87. package/lib/gentle-shell-uninstall.ts +51 -2
  88. package/lib/herdr-activity.ts +24 -0
  89. package/lib/image-attachments.ts +150 -0
  90. package/lib/inprocess-reviewer.ts +33 -2
  91. package/lib/nan-provider.ts +3 -0
  92. package/lib/native-review-cli.ts +20 -5
  93. package/lib/notification-audio-native.ts +149 -13
  94. package/lib/notification-audio.ts +4 -0
  95. package/lib/notification-pulse-worker.ts +82 -18
  96. package/lib/notification-service.ts +5 -1
  97. package/lib/orchestrator-discovery.ts +5 -1
  98. package/lib/orchestrator-presence.ts +87 -8
  99. package/lib/orchestrator-scope.ts +31 -3
  100. package/lib/orchestrator-state.ts +11 -3
  101. package/lib/permission-mode-policy.ts +2 -2
  102. package/lib/provider-contract-bundle.ts +99 -53
  103. package/lib/review-candidate-view-owner.ts +29 -19
  104. package/lib/review-candidate-view.ts +36 -2
  105. package/lib/review-host-relay.ts +11 -2
  106. package/lib/review-reminder-receipt.ts +24 -1
  107. package/lib/review-risk-assessment.ts +43 -38
  108. package/lib/review-session-standing-permission.ts +3 -1
  109. package/lib/session-profile-append-controller.ts +713 -0
  110. package/lib/session-profile-disk-reader.ts +182 -0
  111. package/lib/session-profile-freeze.ts +30 -1
  112. package/lib/session-profile-persistence.ts +15 -7
  113. package/lib/session-scratch.ts +523 -0
  114. package/lib/session-worktree-registry.ts +7 -0
  115. package/lib/shell-bar.ts +29 -28
  116. package/lib/shell-changes-view.ts +80 -13
  117. package/lib/shell-changes.ts +81 -5
  118. package/lib/shell-gauge.ts +49 -6
  119. package/lib/shell-prompt.ts +4 -4
  120. package/lib/shell-sidebar-layout.ts +26 -6
  121. package/lib/shell-todo.ts +24 -5
  122. package/lib/shell-usage-view.ts +5 -1
  123. package/lib/shell-usage.ts +7 -7
  124. package/lib/verifier-admission.ts +30 -0
  125. package/lib/vim-editor-adapter.ts +11 -8
  126. package/lib/voice-artifacts.ts +188 -0
  127. package/lib/voice-command.ts +576 -0
  128. package/lib/voice-install.ts +455 -0
  129. package/lib/voice-platform.ts +99 -0
  130. package/lib/voice-push-to-talk.ts +262 -0
  131. package/lib/voice-session.ts +460 -0
  132. package/lib/voice-settings.ts +50 -0
  133. package/lib/voice-ui.ts +158 -0
  134. package/lib/yolo-session-policy.ts +58 -22
  135. package/lib/yolo-temp-scratch.ts +69 -0
  136. package/package.json +1 -2
  137. package/runtime/gentle-ai-binary.mjs +28 -7
  138. package/runtime/gentle-shell-bundled.mjs +145 -35
  139. package/runtime/gentle-shell-import-pi.mjs +234 -0
  140. package/runtime/gentle-shell-launcher.mjs +152 -13
  141. package/runtime/gentle-shell-startup-update.mjs +61 -9
  142. package/runtime/gentle-shell-uninstall.mjs +51 -2
  143. package/runtime/native-review-cli.mjs +19 -4
  144. package/runtime/review-risk-assessment.mjs +43 -38
  145. package/scripts/build-runtime-modules.mjs +1 -0
  146. package/scripts/bundled-engram-migration.mjs +119 -0
  147. package/scripts/gentle-ai-installer.mjs +10 -10
  148. package/scripts/installer-downloads.mjs +7 -7
  149. package/scripts/installer-preflight.mjs +5 -3
  150. package/scripts/installer-server.mjs +14 -4
  151. package/scripts/installer-windows.mjs +37 -7
  152. package/scripts/main-channel.mjs +94 -19
  153. package/scripts/pi-update.mjs +34 -6
  154. package/scripts/run-test-suite.mjs +5 -0
  155. package/scripts/verify-package-files.mjs +43 -13
  156. package/scripts/voice-worker.cjs +476 -0
  157. package/skills/design/SKILL.md +110 -0
  158. package/skills/gentle-ai/SKILL.md +3 -3
  159. package/skills/work-unit-commits/SKILL.md +5 -3
  160. package/tests/agent-profiles.test.ts +24 -0
  161. package/tests/agent-risk-escalation.test.ts +37 -10
  162. package/tests/agents-config.test.ts +61 -0
  163. package/tests/agents-fake-child.ts +8 -1
  164. package/tests/agents-protocol.test.ts +32 -0
  165. package/tests/agents-runner.test.ts +485 -7
  166. package/tests/agents-session-transport.test.ts +53 -3
  167. package/tests/ask-user-choice.test.ts +30 -0
  168. package/tests/auto-mode.test.ts +17 -0
  169. package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
  170. package/tests/auto-permission-runtime.test.ts +85 -1
  171. package/tests/autonomous-guard.test.ts +142 -0
  172. package/tests/bounded-writer-admission.test.ts +11 -3
  173. package/tests/bundled-install.test.ts +3 -3
  174. package/tests/card-content-policy.test.ts +19 -9
  175. package/tests/child-safety.test.ts +39 -0
  176. package/tests/clipboard-image.test.ts +114 -0
  177. package/tests/codegraph-tools.test.ts +123 -2
  178. package/tests/command-palette.test.ts +15 -5
  179. package/tests/commit-slice-signal.test.ts +162 -0
  180. package/tests/context-backstop.test.ts +225 -0
  181. package/tests/delegate-for-reason-contract.test.ts +92 -24
  182. package/tests/delivered-test-lint.test.ts +105 -0
  183. package/tests/design-canvas-assets.test.ts +710 -0
  184. package/tests/design-canvas-server.test.ts +1096 -0
  185. package/tests/design-comments.test.ts +201 -0
  186. package/tests/design-handoffs.test.ts +282 -0
  187. package/tests/design-open-browser.test.ts +70 -0
  188. package/tests/design-sources.test.ts +132 -0
  189. package/tests/destructive-command-guard.test.ts +85 -1
  190. package/tests/engram-safety.test.ts +132 -57
  191. package/tests/external-editor.test.ts +84 -0
  192. package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
  193. package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
  194. package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
  195. package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
  196. package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
  197. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
  198. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
  199. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
  200. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
  201. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
  202. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
  203. package/tests/gauge-theme.test.ts +120 -0
  204. package/tests/gentle-agents.test.ts +645 -21
  205. package/tests/gentle-ai-binary.test.ts +1 -1
  206. package/tests/gentle-ai-child-guards.test.ts +61 -0
  207. package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
  208. package/tests/gentle-ai-installer.test.ts +51 -51
  209. package/tests/gentle-ai.test.ts +292 -13
  210. package/tests/gentle-design.test.ts +1313 -0
  211. package/tests/gentle-music.test.ts +335 -0
  212. package/tests/gentle-shell-bin.test.ts +163 -0
  213. package/tests/gentle-shell-bundled.test.ts +392 -176
  214. package/tests/gentle-shell-import-pi.test.ts +262 -0
  215. package/tests/gentle-shell-launcher.test.ts +107 -2
  216. package/tests/gentle-shell-startup-update.test.ts +77 -1
  217. package/tests/gentle-shell-uninstall.test.ts +89 -0
  218. package/tests/gentle-shell.test.ts +225 -17
  219. package/tests/gentle-todo.test.ts +190 -0
  220. package/tests/headless-delivery-question.test.ts +58 -0
  221. package/tests/herdr-activity.test.ts +34 -1
  222. package/tests/image-attachments.test.ts +119 -0
  223. package/tests/image-paste-chips.test.ts +236 -0
  224. package/tests/inprocess-reviewer.test.ts +55 -2
  225. package/tests/installer-bundles.test.ts +12 -6
  226. package/tests/installer-posix-bootstrap.test.ts +8 -8
  227. package/tests/installer-preflight.test.ts +16 -0
  228. package/tests/installer-server.test.ts +10 -2
  229. package/tests/installer-windows-bootstrap.test.ts +94 -7
  230. package/tests/installer-windows-diagnostics.test.ts +111 -0
  231. package/tests/main-channel-upgrade.test.ts +232 -2
  232. package/tests/main-channel.test.ts +140 -21
  233. package/tests/nan-provider.test.ts +36 -0
  234. package/tests/native-review-capability-contract.test.ts +17 -4
  235. package/tests/native-review-parity.test.ts +23 -2
  236. package/tests/no-self-pause-contract.test.ts +5 -2
  237. package/tests/notification-audio-native.test.ts +128 -9
  238. package/tests/notification-customize.test.ts +17 -5
  239. package/tests/odd-integration.test.ts +5 -1
  240. package/tests/odd-routing-canonical-ratchet.test.ts +128 -162
  241. package/tests/odd-routing-contract.test.ts +115 -30
  242. package/tests/orchestrator-budget.test.ts +22 -8
  243. package/tests/orchestrator-discovery.test.ts +29 -0
  244. package/tests/orchestrator-presence.test.ts +100 -2
  245. package/tests/orchestrator-rdd-ownership.test.ts +3 -1
  246. package/tests/orchestrator-scope.test.ts +80 -0
  247. package/tests/orchestrator-state.test.ts +29 -0
  248. package/tests/orchestrator-status-publication-contract.test.ts +12 -8
  249. package/tests/package-files.test.ts +34 -0
  250. package/tests/package-manifest.test.ts +24 -20
  251. package/tests/permission-mode-policy.test.ts +10 -1
  252. package/tests/pi-update-launcher.test.ts +21 -2
  253. package/tests/pi-update.test.ts +99 -4
  254. package/tests/primary-session-prompt.test.ts +101 -0
  255. package/tests/provider-contract-bundle.test.ts +118 -0
  256. package/tests/provider-contract-mirror.test.ts +24 -0
  257. package/tests/quiet-tool-rendering.test.ts +23 -0
  258. package/tests/rdd-aware-verification-contract.test.ts +29 -11
  259. package/tests/rdd-status-line.test.ts +79 -0
  260. package/tests/review-agent-end-preflight.test.ts +6 -0
  261. package/tests/review-candidate-view.test.ts +49 -0
  262. package/tests/review-contract-prompt.test.ts +5 -2
  263. package/tests/review-controller-native-routing.test.ts +14 -1
  264. package/tests/review-controller.test.ts +1 -1
  265. package/tests/review-host-relay-routing.test.ts +147 -3
  266. package/tests/review-host-relay.test.ts +9 -2
  267. package/tests/review-json-arguments.test.ts +20 -3
  268. package/tests/review-loadout-rdd-off-sdk.test.ts +258 -0
  269. package/tests/review-owner-identity.test.ts +132 -0
  270. package/tests/review-relay-transport-agent.test.ts +26 -0
  271. package/tests/review-reminder-receipt.test.ts +31 -1
  272. package/tests/review-risk-assessment.test.ts +163 -76
  273. package/tests/review-session-standing-permission-controller.test.ts +14 -1
  274. package/tests/review-session-standing-permission.test.ts +27 -1
  275. package/tests/run-test-suite.test.ts +15 -0
  276. package/tests/runtime-harness.mjs +16 -6
  277. package/tests/session-changes-shell.test.ts +1 -1
  278. package/tests/session-changes.test.ts +2 -1
  279. package/tests/session-profile-append-controller-fork.test.ts +458 -0
  280. package/tests/session-profile-append-controller-pi.test.ts +290 -0
  281. package/tests/session-profile-append-controller.test.ts +731 -0
  282. package/tests/session-profile-disk-reader-pi.test.ts +183 -0
  283. package/tests/session-profile-disk-reader.test.ts +311 -0
  284. package/tests/session-profile-freeze.test.ts +23 -0
  285. package/tests/session-scratch.test.ts +1011 -0
  286. package/tests/session-subject-routing-contract.test.ts +2 -1
  287. package/tests/session-worktree-registry.test.ts +13 -1
  288. package/tests/shell-bar.test.ts +21 -2
  289. package/tests/shell-changes-view.test.ts +137 -1
  290. package/tests/shell-changes.test.ts +148 -1
  291. package/tests/shell-gauge.test.ts +42 -0
  292. package/tests/shell-prompt.test.ts +3 -3
  293. package/tests/shell-render-cache.test.ts +134 -1
  294. package/tests/shell-sidebar-layout.test.ts +50 -0
  295. package/tests/short-check-routing-contract.test.ts +1 -1
  296. package/tests/skill-collision-prefixes.test.ts +3 -1
  297. package/tests/skill-registry-watcher.test.ts +17 -5
  298. package/tests/skill-registry.test.ts +47 -3
  299. package/tests/small-task-inline-contract.test.ts +37 -0
  300. package/tests/standalone-config-home.test.ts +324 -0
  301. package/tests/startup-banner.test.ts +5 -4
  302. package/tests/startup-home-repo.test.ts +34 -0
  303. package/tests/support/orchestrator-modules.ts +1 -0
  304. package/tests/task-size-routing-contract.test.ts +70 -10
  305. package/tests/test-scratch-contract.test.ts +102 -0
  306. package/tests/test-strength-contract.test.ts +36 -1
  307. package/tests/tool-batching-contract.test.ts +64 -0
  308. package/tests/verification-proportionality.test.ts +46 -0
  309. package/tests/verifier-admission.test.ts +154 -0
  310. package/tests/verify-agent-contract.test.ts +8 -8
  311. package/tests/voice-command.test.ts +773 -0
  312. package/tests/voice-install.test.ts +323 -0
  313. package/tests/voice-platform.test.ts +176 -0
  314. package/tests/voice-push-to-talk.test.ts +444 -0
  315. package/tests/voice-session.test.ts +434 -0
  316. package/tests/voice-settings.test.ts +49 -0
  317. package/tests/voice-ui.test.ts +97 -0
  318. package/tests/voice-worker.test.ts +445 -0
  319. package/tests/weak-model-economy-prompt.test.ts +32 -0
  320. package/tests/windows-hidden-processes.test.ts +30 -2
  321. package/tests/writer-edit-surface-scope.test.ts +3 -0
  322. package/tests/writer-feature-document-admission.test.ts +156 -0
  323. package/tests/yolo-customize.test.ts +5 -1
  324. package/tests/yolo-mode.test.ts +19 -1
  325. package/tests/yolo-session-policy-capture.test.ts +104 -0
  326. package/tests/yolo-temp-scratch.test.ts +71 -0
  327. package/scripts/mirror-odd-routing.mjs +0 -242
  328. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
  329. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
  330. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
  331. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  332. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
  333. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
  334. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -1,16 +1,17 @@
1
- import { isAbsolute, join, resolve as resolvePath } from "node:path";
1
+ import { basename, dirname, isAbsolute, join, resolve as resolvePath } from "node:path";
2
2
  import { CHILD_PACKAGE_INJECTION_ENV, encodeChildPackageInjection, type ChildPackageInjection } from "./child-package-injection.ts";
3
+ import { GENTLE_SHELL_CONFIG_HOME_ENV } from "./gentle-shell-bundled.ts";
3
4
 
4
5
  // The gentle-shell launcher: pure, side-effect-free functions over injected
5
6
  // env/fs/exec. `bin/gentle-shell.mjs` (T2) wires these into the real process,
6
7
  // filesystem and child process so this module stays fully unit-testable.
7
8
 
8
- export type LauncherCommand = "home" | "setup" | "upgrade" | "self-uninstall";
9
+ export type LauncherCommand = "home" | "setup" | "upgrade" | "self-uninstall" | "memory";
9
10
 
10
11
  // pi's own package-management subcommands (see pi's cli/args.ts printHelp
11
12
  // "Commands" list): each is dispatched by pi itself, before pi's own flag
12
13
  // parsing, purely on argv[0]. `uninstall` is pi's alias for `remove`.
13
- export const PI_SUBCOMMANDS = ["install", "remove", "uninstall", "update", "list", "config", "auth"] as const;
14
+ export const PI_SUBCOMMANDS = ["install", "remove", "uninstall", "update", "list", "config", "auth", "mcp"] as const;
14
15
 
15
16
  export type PiSubcommand = (typeof PI_SUBCOMMANDS)[number];
16
17
 
@@ -41,7 +42,7 @@ export interface ParsedLauncherArgs {
41
42
  // untouched as commandArgs — T2 owns interpreting `home link|isolated|<path>`.
42
43
  // `self-uninstall` is not `uninstall`, which stays pi's own alias for `remove`.
43
44
  export function parseLauncherArgs(argv: string[]): ParsedLauncherArgs {
44
- if (argv[0] === "home" || argv[0] === "upgrade" || argv[0] === "self-uninstall") {
45
+ if (argv[0] === "home" || argv[0] === "upgrade" || argv[0] === "self-uninstall" || argv[0] === "memory") {
45
46
  return {
46
47
  link: false,
47
48
  isolated: false,
@@ -76,6 +77,9 @@ export function parseLauncherArgs(argv: string[]): ParsedLauncherArgs {
76
77
  if (passthrough.length === 0 && rest.length > 0 && isPiSubcommand(rest[0])) {
77
78
  piSubcommand = rest[0];
78
79
  }
80
+ // After a pi subcommand the separator belongs to it, e.g.
81
+ // `mcp add <server> -- <command>`; only a leading one is the launcher's.
82
+ if (piSubcommand !== undefined && passthrough.length > 0) passthrough.push(arg);
79
83
  passthrough.push(...rest);
80
84
  break;
81
85
  }
@@ -147,6 +151,14 @@ export function parseLauncherArgs(argv: string[]): ParsedLauncherArgs {
147
151
  commandArgs = argv.slice(i + 1);
148
152
  break;
149
153
  }
154
+ // `home import-pi` likewise takes the home selectors ahead of it, so it
155
+ // imports into the home they resolve to; any other `home` after a
156
+ // selector stays pi's.
157
+ if (arg === "home" && argv[i + 1] === "import-pi" && command === undefined && passthrough.length === 0) {
158
+ command = "home";
159
+ commandArgs = argv.slice(i + 1);
160
+ break;
161
+ }
150
162
  if (passthrough.length === 0 && isPiSubcommand(arg)) {
151
163
  piSubcommand = arg;
152
164
  }
@@ -211,6 +223,77 @@ export function isolatedDir(env: Record<string, string | undefined>, homedir: st
211
223
  return env.GENTLE_SHELL_HOME || join(homedir, ".gentle-shell", "agent");
212
224
  }
213
225
 
226
+ // #2201: the isolated home keeps Gentle Shell's config in a gentle-ai folder
227
+ // next to its agent dir (~/.gentle-shell/gentle-ai by default), given to Pi as
228
+ // GENTLE_SHELL_CONFIG_HOME_ENV. `--link` and a custom `--home` keep the shared
229
+ // ~/.pi/gentle-ai: undefined for them.
230
+ export function standaloneConfigHome(home: ResolvedHome): string | undefined {
231
+ if (home.mode !== "isolated") return undefined;
232
+ const dir = resolvePath(home.dir);
233
+ const parent = dirname(dir);
234
+ // Only inside a .gentle-shell folder is the sibling Gentle Shell's own: next
235
+ // to a custom GENTLE_SHELL_HOME (~/myagent) it would adopt ~/gentle-ai, which
236
+ // may be anyone's, so such homes keep the shared config home.
237
+ return basename(dir) === "agent" && basename(parent) === ".gentle-shell" ? join(parent, "gentle-ai") : undefined;
238
+ }
239
+
240
+ // The filesystem behind migrateStandaloneConfig; bin/gentle-shell.mjs wires the real one.
241
+ export interface ConfigMigrationFs {
242
+ // lstat: a symbolic link is "other", never followed.
243
+ kind: (path: string) => "missing" | "dir" | "file" | "other";
244
+ // The entry names of a directory (following a linked directory); throws when unreadable.
245
+ list: (path: string) => string[];
246
+ // mkdir -p with mode 0700; throws on failure.
247
+ mkdirPrivate: (path: string) => void;
248
+ // Copies a regular file, never overwriting `to`; throws on failure.
249
+ copyNew: (from: string, to: string) => void;
250
+ }
251
+
252
+ // Install-level main-channel state stays in ~/.pi/gentle-ai: the installer and
253
+ // `gentle-shell upgrade` write it outside any session (gentleAiDevBinaryRegistrationPath).
254
+ const UNMIGRATED_CONFIG_FILES = new Set(["dev-binary.json", "channel.json"]);
255
+
256
+ // #2201: the first isolated launch after the move keeps the user's settings.
257
+ // Only when `target` does not exist yet: it is created (0700), and the regular
258
+ // files of `source` are copied into it, never overwriting and never changing
259
+ // `source`, which stays for Pi; symbolic links, subdirectories and the
260
+ // main-channel files are skipped. Once the folder exists this does nothing, so
261
+ // it is a one-time snapshot. A failure is reported in `lines`, never thrown:
262
+ // it never blocks the launch.
263
+ export function migrateStandaloneConfig(input: { source: string; target: string; fs: ConfigMigrationFs }): { lines: string[] } {
264
+ const { source, target, fs } = input;
265
+ if (fs.kind(target) !== "missing") return { lines: [] };
266
+ const reason = (error: unknown) => (error instanceof Error ? error.message : String(error));
267
+ try {
268
+ fs.mkdirPrivate(target);
269
+ } catch (error) {
270
+ return { lines: [`gentle-shell: could not create ${target} for your Gentle Shell settings (${reason(error)}); continuing`] };
271
+ }
272
+ if (fs.kind(source) === "missing") return { lines: [] };
273
+ let names: string[];
274
+ try {
275
+ names = fs.list(source).sort();
276
+ } catch (error) {
277
+ return { lines: [`gentle-shell: could not copy your settings from ${source} to ${target} (${reason(error)}); continuing`] };
278
+ }
279
+ const copied: string[] = [];
280
+ const failed: string[] = [];
281
+ for (const name of names) {
282
+ if (UNMIGRATED_CONFIG_FILES.has(name) || fs.kind(join(source, name)) !== "file") continue;
283
+ try {
284
+ fs.copyNew(join(source, name), join(target, name));
285
+ copied.push(name);
286
+ } catch (error) {
287
+ failed.push(`${name}: ${reason(error)}`);
288
+ }
289
+ }
290
+ if (failed.length > 0) {
291
+ return { lines: [`gentle-shell: could not copy ${failed.join("; ")} from ${source} to ${target}; continuing with ${copied.length} copied file(s)`] };
292
+ }
293
+ if (copied.length === 0) return { lines: [] };
294
+ return { lines: [`gentle-shell: copied ${copied.length} settings file(s) from ${source} to ${target}, Gentle Shell's own config folder from now on (one-time copy; ${source} stays for Pi)`] };
295
+ }
296
+
214
297
  export function resolveHome(input: ResolveHomeInput): ResolvedHome {
215
298
  const { args, env, homedir, config } = input;
216
299
 
@@ -241,6 +324,15 @@ export function homeSelectorFlags(home: ResolvedHome): string[] {
241
324
  return [];
242
325
  }
243
326
 
327
+ // The first-run line of a separate home. `home import-pi` is offered only
328
+ // when the user's pi home has sign-ins (auth.json) to copy.
329
+ export function separateHomeHint(home: ResolvedHome, input: { piSignIns: boolean }): string {
330
+ const base = `gentle-shell: using a separate home at ${home.dir}. Run 'gentle-shell --link' to reuse your pi sign-ins and chats`;
331
+ if (!input.piSignIns) return `${base}.`;
332
+ const command = ["gentle-shell", ...homeSelectorFlags(home).map(shellQuote), "home", "import-pi"].join(" ");
333
+ return `${base}, or '${command}' to copy your sign-ins and MCP servers into this home.`;
334
+ }
335
+
244
336
  export function launcherConfigPath(homedir: string): string {
245
337
  return join(homedir, ".gentle-shell", "config.json");
246
338
  }
@@ -575,6 +667,20 @@ const POST_INSTALL_REMOVAL_PACKAGES: readonly { readonly name: string; readonly
575
667
  // bin/gentle-shell.mjs.
576
668
  export const POST_INSTALL_REMOVAL_SOURCES: readonly string[] = POST_INSTALL_REMOVAL_PACKAGES.map((entry) => entry.source);
577
669
 
670
+ // #2143: a Gentle AI with package exclusion (gentle-ai#5503) skips these
671
+ // managed Pi packages instead of installing them only for the post-install
672
+ // removal above to delete them. An older Gentle AI ignores the variable (a flag
673
+ // would be rejected), so the removal stays as the fallback. Only sources Gentle
674
+ // AI manages belong here: it rejects any other value.
675
+ export const GENTLE_AI_PI_EXCLUDE_PACKAGES_ENV = "GENTLE_AI_PI_EXCLUDE_PACKAGES";
676
+ const LAUNCHER_EXCLUDED_PI_PACKAGES: readonly string[] = ["npm:gentle-pi"];
677
+
678
+ export function withLauncherPackagesExcluded(env: Record<string, string | undefined>): Record<string, string | undefined> {
679
+ const own = (env[GENTLE_AI_PI_EXCLUDE_PACKAGES_ENV] ?? "").split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
680
+ const merged = [...own, ...LAUNCHER_EXCLUDED_PI_PACKAGES.filter((source) => !own.includes(source))];
681
+ return { ...env, [GENTLE_AI_PI_EXCLUDE_PACKAGES_ENV]: merged.join(",") };
682
+ }
683
+
578
684
  // Scans a settings.json `packages` list (same string/object-source parsing
579
685
  // as settingsDeclareGentlePi/findGentlePiDeclaration above) for any entry
580
686
  // whose npm package name matches POST_INSTALL_REMOVAL_PACKAGES, at any
@@ -875,10 +981,13 @@ export interface BuildPiInvocationInput {
875
981
  passthrough: string[];
876
982
  // Set when parseLauncherArgs recognised passthrough[0] as one of
877
983
  // PI_SUBCOMMANDS. pi dispatches install/remove/uninstall/update/list/
878
- // config/auth on argv[0] before its own flag parsing, so none of the
984
+ // config/auth/mcp on argv[0] before its own flag parsing, so none of the
879
985
  // gentle-pi extension injection below may precede it.
880
986
  piSubcommand?: PiSubcommand;
881
987
  baseEnv: Record<string, string | undefined>;
988
+ // A bundled launch: its gentle-pi keeps its config in the prefix, so Pi gets
989
+ // no standalone config marker (standaloneConfigHome).
990
+ bundled?: boolean;
882
991
  // The OS home behind userPiHome's conventional ~/.pi/agent fallback.
883
992
  homedir: string;
884
993
  // The directory pi is spawned in, which pi resolves a relative -e path
@@ -896,7 +1005,7 @@ export interface PiInvocation {
896
1005
 
897
1006
  // Four cases, checked in this order — `piSubcommand` first, then `takeOver`:
898
1007
  // - piSubcommand: pi dispatches install/remove/uninstall/update/list/
899
- // config/auth on argv[0] before it even parses flags, so any injected
1008
+ // config/auth/mcp on argv[0] before it even parses flags, so any injected
900
1009
  // -e flag ahead of it stops pi from
901
1010
  // recognising its subcommand at all — this is exactly the observed
902
1011
  // 2026-09-22 bug where `gentle-shell install npm:x` opened an
@@ -977,6 +1086,12 @@ export function buildPiInvocation(input: BuildPiInvocationInput): PiInvocation {
977
1086
  GENTLE_PI_AGENT_HOME: input.home.dir,
978
1087
  [USER_PI_HOME_ENV]: userPiHome(input.baseEnv, input.homedir),
979
1088
  };
1089
+ // #2201: only the standalone isolated home gets its own config folder; any
1090
+ // other launch removes an inherited marker, so a nested `--link` launch from
1091
+ // an isolated session reads ~/.pi/gentle-ai again.
1092
+ const configHome = input.bundled === true ? undefined : standaloneConfigHome(input.home);
1093
+ if (configHome === undefined) delete env[GENTLE_SHELL_CONFIG_HOME_ENV];
1094
+ else env[GENTLE_SHELL_CONFIG_HOME_ENV] = configHome;
980
1095
  if (childInjection === undefined) delete env[CHILD_PACKAGE_INJECTION_ENV];
981
1096
  else env[CHILD_PACKAGE_INJECTION_ENV] = encodeChildPackageInjection(childInjection);
982
1097
 
@@ -1003,12 +1118,17 @@ const CMD_EXE_SPECIAL_CHARS = /[\s"&|<>^%()]/;
1003
1118
 
1004
1119
  // cmd.exe quoting is deliberately simple, not a full cmd.exe parser: wrap a
1005
1120
  // token in double quotes when it is empty or contains whitespace or any of
1006
- // `"&|<>^%()`, and escape an inner `"` as `\"` — doubling inner quotes is not
1007
- // reliable in cmd.exe, unlike the `\"` convention Node's own Windows spawn
1008
- // helpers use.
1121
+ // `&|<>^%()`. A token with an inner `"` cannot be represented: cmd.exe has no
1122
+ // backslash escape, so `\"` still ends the quoted section and the rest of the
1123
+ // token is parsed as command syntax (`x" & whoami & rem "` would run
1124
+ // `whoami`). Doubling the quote is not reliable either, so such a token is
1125
+ // refused instead of producing a command line it cannot protect.
1009
1126
  export function quoteForCmdExe(token: string): string {
1127
+ if (token.includes('"')) {
1128
+ throw new Error("cannot pass an argument containing a double quote through cmd.exe: it would end the quoted argument and let the rest run as a command");
1129
+ }
1010
1130
  if (token.length > 0 && !CMD_EXE_SPECIAL_CHARS.test(token)) return token;
1011
- return `"${token.replace(/"/g, '\\"')}"`;
1131
+ return `"${token}"`;
1012
1132
  }
1013
1133
 
1014
1134
  const POSIX_SHELL_SPECIAL_CHARS = /[\s"'`\\$&|;<>(){}*?[\]!#~]/;
@@ -1052,7 +1172,7 @@ export function planSpawn(input: PlanSpawnInput): SpawnPlan {
1052
1172
  // formatting as practical instead of imposing its own. `indent` is
1053
1173
  // `undefined` for compact (no-whitespace) JSON, matching what
1054
1174
  // `JSON.stringify(value)` (no third argument) produces.
1055
- function detectJsonFormatting(text: string): { indent: string | undefined; trailingNewline: boolean } {
1175
+ export function detectJsonFormatting(text: string): { indent: string | undefined; trailingNewline: boolean } {
1056
1176
  const match = text.match(/\{\r?\n([ \t]+)/);
1057
1177
  return { indent: match ? match[1] : undefined, trailingNewline: text.endsWith("\n") };
1058
1178
  }
@@ -1174,9 +1294,11 @@ export function helpText(): string {
1174
1294
  return [
1175
1295
  "Usage: gentle-shell [options] [-- pi-args...]",
1176
1296
  " gentle-shell home [link|isolated|<path>]",
1297
+ " gentle-shell [home selectors] home import-pi [--dry-run]",
1177
1298
  " gentle-shell [home selectors] setup [--dry-run]",
1178
1299
  " gentle-shell upgrade [--channel release|main]",
1179
1300
  " gentle-shell upgrade --rollback",
1301
+ " gentle-shell memory migrate --confirm-quiescent",
1180
1302
  " gentle-shell self-uninstall [--dry-run] [--yes] [--include-shared]",
1181
1303
  "",
1182
1304
  "Opens pi with the Gentle Shell package loaded, without touching your",
@@ -1184,7 +1306,8 @@ export function helpText(): string {
1184
1306
  "",
1185
1307
  "Options:",
1186
1308
  " --link Use your existing pi agent home (never edits its settings.json).",
1187
- " --isolated Use the dedicated ~/.gentle-shell/agent home (default).",
1309
+ " --isolated Use the dedicated ~/.gentle-shell/agent home (default), with its own",
1310
+ " settings in ~/.gentle-shell/gentle-ai (copied once from ~/.pi/gentle-ai).",
1188
1311
  " --home <path> Use a custom agent home directory.",
1189
1312
  " --package-root <dir> Force this directory as the gentle-pi package to load, taking over",
1190
1313
  " from any conflicting package the target settings.json already declares.",
@@ -1193,6 +1316,11 @@ export function helpText(): string {
1193
1316
  "",
1194
1317
  "Commands:",
1195
1318
  " home Print or persist the effective home mode (link, isolated, or a path).",
1319
+ " home import-pi Copy your pi sign-ins (auth.json) and MCP servers (mcp.json) into the",
1320
+ " resolved separate home, plus your default provider, model and thinking",
1321
+ " level when it has none. Never overwrites a file or setting, never copies",
1322
+ " packages, extensions or caches, and never changes your pi home.",
1323
+ " --dry-run only shows the plan. Refuses --link (nothing to import).",
1196
1324
  " setup Provision the resolved home with the gentle-ai companion packages",
1197
1325
  " (runs the package-local gentle-ai 'install --agent pi --scope global').",
1198
1326
  " Accepts --dry-run, forwarded to gentle-ai. Accepts a home selector",
@@ -1204,9 +1332,15 @@ export function helpText(): string {
1204
1332
  " installer's own Node, pnpm and Pi) follows the release channel only:",
1205
1333
  " it installs the new version beside the current one, verifies it, then",
1206
1334
  " switches; --rollback switches back to the previous kept version.",
1335
+ " memory migrate Explicit bundled-store recovery using the verified core's migrate-copy.",
1336
+ " Requires --confirm-quiescent: stop ALL Engram clients first. Keeps the",
1337
+ " original directory as a backup; never runs automatically on upgrade.",
1338
+ " Requires a bundled core release supporting migrate-copy; older cores",
1339
+ " fail closed without changing the source or its provenance.",
1207
1340
  " self-uninstall Remove Gentle Shell: its own data, the isolated home and the gentle-pi",
1208
- " package. Shows the plan first and asks before removing (--yes skips",
1209
- " the question). The shared ~/.pi/gentle-ai configuration is kept unless",
1341
+ " package, with its settings in ~/.gentle-shell/gentle-ai. Shows the plan",
1342
+ " first and asks before removing (--yes skips the question). The shared",
1343
+ " ~/.pi/gentle-ai configuration is kept unless",
1210
1344
  " you accept or pass --include-shared; --dry-run only shows the plan.",
1211
1345
  " Never touches your pi home or custom --home homes.",
1212
1346
  "",
@@ -1219,6 +1353,8 @@ export function helpText(): string {
1219
1353
  " runs 'gentle-shell upgrade' instead of updating Pi.",
1220
1354
  " gentle-shell config Run pi's own 'config' against the resolved home.",
1221
1355
  " gentle-shell auth <command> Run pi's own 'auth' against the resolved home.",
1356
+ " gentle-shell mcp <command> Run pi's own 'mcp' (add, list, login, logout) against the",
1357
+ " resolved home, e.g. gentle-shell mcp add <name> -- <command>.",
1222
1358
  " 'gentle-shell uninstall <source>' is pi's own alias for 'remove'; to remove Gentle",
1223
1359
  " Shell itself, use 'gentle-shell self-uninstall'.",
1224
1360
  " These run pi's own commands, forwarded verbatim, against the --isolated home",
@@ -1233,7 +1369,10 @@ export function helpText(): string {
1233
1369
  "",
1234
1370
  "Environment variables:",
1235
1371
  " GENTLE_SHELL_PI Path to the pi executable to run.",
1236
- " GENTLE_SHELL_HOME Directory for the isolated home (default: ~/.gentle-shell/agent).",
1372
+ " GENTLE_SHELL_HOME Directory for the isolated home (default: ~/.gentle-shell/agent);",
1373
+ " inside a .gentle-shell folder, its settings live in the gentle-ai",
1374
+ " folder next to it; elsewhere they stay in ~/.pi/gentle-ai.",
1375
+ " GENTLE_PI_CONFIG_HOME Gentle Shell settings folder for every home, overriding the above.",
1237
1376
  " PI_CODING_AGENT_DIR Directory for the --link home, shared with pi itself.",
1238
1377
  "",
1239
1378
  "Every other argument is forwarded to pi unchanged.",
@@ -19,8 +19,32 @@ export interface StartupUpdateLookupOptions {
19
19
  skipped?: string;
20
20
  lookup: (signal: AbortSignal) => Promise<string | undefined>;
21
21
  timeoutMs?: number;
22
+ /** A recorded previous lookup. Fresh enough, it answers without the network. */
23
+ cache?: StartupReleaseCache;
24
+ /** Injected clock and budget for tests; defaults to now and one day. */
25
+ nowMs?: number;
26
+ ttlMs?: number;
27
+ /** Called once with a live lookup's version; never for a fresh-cache answer or an unanswered lookup. */
28
+ cacheWrite?: (latest: string) => void;
22
29
  }
23
30
 
31
+ /** The launcher config records the last live lookup so the everyday launch stays off the network. */
32
+ export interface StartupReleaseCache {
33
+ checkedAt: number;
34
+ latest: string;
35
+ }
36
+
37
+ /** Reads the config's `startupReleaseCheck` key; anything malformed is no cache. */
38
+ export function parseStartupReleaseCache(value: unknown): StartupReleaseCache | undefined {
39
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
40
+ const { checkedAt, latest } = value as { checkedAt?: unknown; latest?: unknown };
41
+ const ms = typeof checkedAt === "number" ? checkedAt : typeof checkedAt === "string" ? Date.parse(checkedAt) : Number.NaN;
42
+ if (!Number.isFinite(ms) || typeof latest !== "string" || latest === "") return undefined;
43
+ return { checkedAt: ms, latest };
44
+ }
45
+
46
+ export const STARTUP_RELEASE_TTL_MS = 24 * 60 * 60 * 1000;
47
+
24
48
  export function shouldCheckStartupUpdate({ args, env, bundled, inputTTY, outputTTY }: StartupUpdateCheckOptions): boolean {
25
49
  if (!bundled || !inputTTY || !outputTTY || args.command || args.piSubcommand || args.error || args.help || args.version || args.packageRoot !== undefined) {
26
50
  return false;
@@ -61,9 +85,30 @@ function isNewer(latest: string[], current: string[]): boolean {
61
85
  return false;
62
86
  }
63
87
 
64
- export async function findStartupUpdate({ current, skipped, lookup, timeoutMs = 1500 }: StartupUpdateLookupOptions): Promise<StartupUpdateOffer | undefined> {
88
+ function offerFor(current: string, latest: string | undefined, skipped: string | undefined): StartupUpdateOffer | undefined {
65
89
  const currentParts = stableParts(current);
66
90
  if (!currentParts) return undefined;
91
+ if (latest === undefined || latest === skipped) return undefined;
92
+ const latestParts = stableParts(latest);
93
+ if (!latestParts || !isNewer(latestParts, currentParts)) return undefined;
94
+ return {
95
+ current,
96
+ latest,
97
+ notesUrl: `https://github.com/Gentleman-Programming/gentle-shell/releases/tag/v${latest}`,
98
+ };
99
+ }
100
+
101
+ // A malformed cached latest is never trusted — it must not suppress the
102
+ // network check for a whole TTL window — so it counts as no cache at all.
103
+ function usableCache(cache: StartupReleaseCache | undefined, ttlMs: number, nowMs: number): StartupReleaseCache | undefined {
104
+ if (cache === undefined) return undefined;
105
+ if (!Number.isFinite(cache.checkedAt) || typeof cache.latest !== "string" || cache.latest === "" || stableParts(cache.latest) === undefined) return undefined;
106
+ return nowMs - cache.checkedAt < ttlMs ? cache : undefined;
107
+ }
108
+
109
+ export async function findStartupUpdate({ current, skipped, lookup, timeoutMs = 1500, cache, nowMs = Date.now(), ttlMs = STARTUP_RELEASE_TTL_MS, cacheWrite }: StartupUpdateLookupOptions): Promise<StartupUpdateOffer | undefined> {
110
+ const cached = usableCache(cache, ttlMs, nowMs);
111
+ if (cached !== undefined) return offerFor(current, cached.latest, skipped);
67
112
  const controller = new AbortController();
68
113
  const deadline = Number.isFinite(timeoutMs) && timeoutMs >= 0 && timeoutMs <= 2_147_483_647 ? timeoutMs : 1500;
69
114
  let timer: ReturnType<typeof setTimeout> | undefined;
@@ -76,17 +121,24 @@ export async function findStartupUpdate({ current, skipped, lookup, timeoutMs =
76
121
  });
77
122
  // The race bounds startup even if the injected lookup ignores cancellation.
78
123
  const latest = await Promise.race([lookup(controller.signal), timeout]);
79
- if (latest === undefined || latest === skipped) return undefined;
80
- const latestParts = stableParts(latest);
81
- if (!latestParts || !isNewer(latestParts, currentParts)) return undefined;
82
- return {
83
- current,
84
- latest,
85
- notesUrl: `https://github.com/Gentleman-Programming/gentle-shell/releases/tag/v${latest}`,
86
- };
124
+ // Only a real answer is worth a day's trust: a timeout or rejection must
125
+ // not suppress tomorrow's check.
126
+ if (typeof latest === "string") cacheWrite?.(latest);
127
+ return offerFor(current, latest, skipped);
87
128
  } catch {
88
129
  return undefined;
89
130
  } finally {
90
131
  clearTimeout(timer);
91
132
  }
92
133
  }
134
+
135
+ // Mirrors omitStartupVersion: preserves every other config key, fails loudly on
136
+ // an invalid config, and records the lookup's answer with an injected clock.
137
+ export function writeStartupReleaseCache(read: () => string | undefined, write: (text: string) => void, version: string, nowMs = Date.now()): void {
138
+ const text = read();
139
+ const config = text === undefined ? {} : JSON.parse(text);
140
+ if (config === null || typeof config !== "object" || Array.isArray(config)) throw new Error("Invalid launcher config");
141
+ const cache: StartupReleaseCache = { checkedAt: nowMs, latest: version };
142
+ const serialized = JSON.stringify({ ...config, startupReleaseCheck: { checkedAt: new Date(cache.checkedAt).toISOString(), latest: cache.latest } }, null, 2);
143
+ write(`${serialized}\n`);
144
+ }
@@ -1,6 +1,8 @@
1
- import { lstatSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmdirSync, rmSync } from "node:fs";
1
+ import { lstatSync, readdirSync, readFileSync, readlinkSync, realpathSync, renameSync, rmdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { randomUUID } from "node:crypto";
2
3
  import { createInterface } from "node:readline";
3
4
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
5
+ import { withoutBundledNpmCommand } from "./gentle-shell-bundled.ts";
4
6
  import { isolatedDir, launcherConfigPath, parseRawLauncherConfig, provisionedEntry, userPiHome } from "./gentle-shell-launcher.ts";
5
7
 
6
8
  // `gentle-shell self-uninstall`: removes what Gentle Shell itself created, then
@@ -263,6 +265,14 @@ async function planSelfUninstall(input: SelfUninstallInput, configHome: string,
263
265
  { path: gentleAiState, label: "Gentle AI's state", inside: true },
264
266
  ];
265
267
  const defaultConfigHome = canonical(join(homedir, ".pi", "gentle-ai"));
268
+ // #2201: the standalone launcher keeps the isolated home's config in a
269
+ // gentle-ai folder next to it (standaloneConfigHome). Next to the default
270
+ // home it lies inside ~/.gentle-shell and is Gentle Shell's by location;
271
+ // next to a GENTLE_SHELL_HOME it may be anyone's, so it is only listed.
272
+ // When GENTLE_PI_CONFIG_HOME names it, the config home's rules apply instead.
273
+ const defaultHome = resolve(isolated) === resolve(join(homedir, ".gentle-shell", "agent"));
274
+ const ownConfig = join(dirname(isolated), "gentle-ai");
275
+ const ownConfigSeparate = comparable(canonical(ownConfig), platform) !== comparable(canonical(configHome), platform);
266
276
  const candidates = [
267
277
  {
268
278
  who: env.GENTLE_SHELL_HOME ? `GENTLE_SHELL_HOME points at ${isolated}` : `The isolated home ${isolated}`,
@@ -278,6 +288,12 @@ async function planSelfUninstall(input: SelfUninstallInput, configHome: string,
278
288
  allowedInsidePi: defaultConfigHome,
279
289
  },
280
290
  { who: `The launcher folder ${shellDirectory}`, path: shellDirectory, guards, allowedInsidePi: undefined },
291
+ ...(defaultHome && ownConfigSeparate ? [{
292
+ who: `Gentle Shell's config folder ${ownConfig}`,
293
+ path: ownConfig,
294
+ guards: [...guards, { path: configHome, label: "the Gentle AI config home", inside: true }],
295
+ allowedInsidePi: undefined,
296
+ }] : []),
281
297
  ];
282
298
  for (const candidate of candidates) {
283
299
  const real = canonical(candidate.path);
@@ -312,11 +328,14 @@ async function planSelfUninstall(input: SelfUninstallInput, configHome: string,
312
328
 
313
329
  // The default isolated home is Gentle Shell's by location; a GENTLE_SHELL_HOME
314
330
  // directory only with the launcher's ownership marker or setup record.
315
- const defaultHome = resolve(isolated) === resolve(join(homedir, ".gentle-shell", "agent"));
316
331
  const owned = defaultHome || inspect(join(isolated, HOME_OWNERSHIP_MARKER), "file") === "ok"
317
332
  || provisionedEntry(launcherConfig, isolatedReal) !== undefined;
318
333
  if (owned || inspect(isolated, "dir") !== "ok") consider({ path: isolated, label: "isolated home: its sign-ins, chats and settings", kind: "dir" });
319
334
  else plan.kept.push(`${isolated}: not created by Gentle Shell (no ${HOME_OWNERSHIP_MARKER} marker or setup record)`);
335
+ if (defaultHome && ownConfigSeparate) consider({ path: ownConfig, label: "Gentle Shell's own configuration: profiles, banner and other preferences", kind: "dir" });
336
+ else if (ownConfigSeparate && inspect(ownConfig, "dir") !== "missing") {
337
+ plan.kept.push(`${ownConfig}: next to GENTLE_SHELL_HOME, so it may not be Gentle Shell's; remove it yourself if it holds only Gentle Shell settings`);
338
+ }
320
339
  consider({ path: configPath, label: "launcher settings", kind: "file" });
321
340
 
322
341
  for (const name of SHARED_CONFIG_FILES) {
@@ -761,6 +780,18 @@ function printBundledPlan(plan: BundledPlan, input: BundledSelfUninstallInput, o
761
780
  }
762
781
  }
763
782
 
783
+ /** Replaces `path`'s text through a same-directory temporary file, so a crash never leaves it half-written. */
784
+ function writeTextAtomically(path: string, text: string): void {
785
+ const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`);
786
+ try {
787
+ writeFileSync(temporary, text);
788
+ renameSync(temporary, path);
789
+ } catch (error) {
790
+ try { unlinkSync(temporary); } catch { /* The temporary file is best effort. */ }
791
+ throw error;
792
+ }
793
+ }
794
+
764
795
  /**
765
796
  * Runs `gentle-shell self-uninstall` for the bundled install at `root`. Exit
766
797
  * codes as runSelfUninstall: 0 done or dry run, 1 refused, cancelled or failed,
@@ -820,6 +851,24 @@ export async function runBundledSelfUninstall(input: BundledSelfUninstallInput):
820
851
  } catch {
821
852
  // A leftover empty folder is harmless.
822
853
  }
854
+ // A home that stays must not keep Pi pointed at the npm this uninstall removed
855
+ // (#2469): an `npmCommand` naming the prefix fails every later `pi install`
856
+ // with ENOENT naming the deleted node.exe.
857
+ if (!confirmation.includeShared) {
858
+ const settingsPath = join(plan.shellHome, "agent", "settings.json");
859
+ let settingsText: string | undefined;
860
+ try { settingsText = readFileSync(settingsPath, "utf8"); } catch { settingsText = undefined; }
861
+ const removal = withoutBundledNpmCommand(settingsText, (path) => within(root, path, platform));
862
+ if (removal.status === "removed") {
863
+ try {
864
+ writeTextAtomically(settingsPath, removal.text);
865
+ out(`Removed npmCommand from ${settingsPath}: it named the removed install.`);
866
+ } catch (error) {
867
+ err(`gentle-shell self-uninstall: could not remove npmCommand from ${settingsPath}: ${error instanceof Error ? error.message : String(error)}`);
868
+ err(`Pi package installs keep failing until the npmCommand naming ${root} is deleted from it.`);
869
+ }
870
+ }
871
+ }
823
872
  if (plan.held.length > 0) {
824
873
  try {
825
874
  if (!input.deferRemoval) throw new Error("no deferred removal on this system");
@@ -111,6 +111,30 @@ export function activitySummary(tasks: unknown, columns = FALLBACK_COLUMNS): str
111
111
  return `${first}\n ${second}${overflow ? "…" : ""}`;
112
112
  }
113
113
 
114
+ /** Herdr pane environment: HERDR_ENV=1 plus nonblank pane and socket variables. */
115
+ export function herdrDetected(env: NodeJS.ProcessEnv): boolean {
116
+ return env.HERDR_ENV === "1" && !!env.HERDR_PANE_ID?.trim() && !!env.HERDR_SOCKET_PATH?.trim();
117
+ }
118
+
119
+ /** Read-only /gentle:doctor guidance; empty outside Herdr. Never touches Herdr config. */
120
+ export function herdrSetupHint(
121
+ env: NodeJS.ProcessEnv,
122
+ session: { interactive: boolean; isSocket: (path: string) => boolean },
123
+ ): string[] {
124
+ if (!herdrDetected(env)) return [];
125
+ const inactive = env.GENTLE_PI_AGENTS_CHILD === "1" ? "Gentle Agents child session"
126
+ : !session.interactive ? "requires the interactive Pi TUI"
127
+ : !session.isSocket(env.HERDR_SOCKET_PATH!) ? "HERDR_SOCKET_PATH is not a Unix socket" : undefined;
128
+ return [
129
+ "Herdr",
130
+ inactive ? `warn: Herdr task-summary publishing inactive: ${inactive}` : "pass: Herdr task-summary publishing active",
131
+ "info: Herdr's default sidebar does not show task titles; add these two rows to the Agents sidebar rows in your Herdr config: ['$summary'], ['$summary2']",
132
+ "info: Sidebar rows belong to the config of the Herdr client that displays the session, including when Pi runs over SSH",
133
+ "info: After editing, reload or restart Herdr; Gentle Shell never changes Herdr config",
134
+ "info: Details: https://github.com/Gentleman-Programming/gentle-shell/blob/main/docs/readme-reference.md#herdr-active-work-summary",
135
+ ];
136
+ }
137
+
114
138
  export function metadataArgs(pane: string, summary: string | null, seq: number): string[] {
115
139
  const rows = summary?.split("\n") ?? [];
116
140
  const tokens = ["summary", "summary2"].flatMap((token, index) => rows[index]