gentle-pi 4.0.0 → 4.2.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 (425) hide show
  1. package/README.md +157 -18
  2. package/assets/agents/gentle-ai-explore.md +11 -4
  3. package/assets/agents/gentle-ai-verify.md +23 -4
  4. package/assets/agents/gentle-ai-worker.md +6 -6
  5. package/assets/install-wizard/index.html +47 -0
  6. package/assets/install-wizard/wizard.css +1279 -0
  7. package/assets/install-wizard/wizard.js +951 -0
  8. package/assets/orchestrator-delegation.md +70 -163
  9. package/assets/orchestrator-memory.md +11 -4
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-tracking.md +53 -0
  12. package/assets/orchestrator-verification.md +28 -0
  13. package/assets/orchestrator-writer.md +38 -0
  14. package/assets/orchestrator.md +29 -25
  15. package/assets/sounds/LICENSE.md +52 -0
  16. package/assets/sounds/attention.wav +0 -0
  17. package/assets/sounds/error.wav +0 -0
  18. package/assets/sounds/success.wav +0 -0
  19. package/bin/engram-guard +9 -0
  20. package/bin/gentle-shell-install.mjs +302 -0
  21. package/bin/gentle-shell.mjs +708 -22
  22. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  23. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  24. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  25. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  27. package/docs/auto-permission-answers.md +25 -0
  28. package/docs/auto-permission-evidence.md +39 -0
  29. package/docs/bundled-install.md +399 -0
  30. package/docs/delegated-verification.md +3 -5
  31. package/docs/gentle-agents-activity.md +513 -0
  32. package/docs/gentle-shell-uninstall.md +136 -0
  33. package/docs/gentle-shell.md +90 -33
  34. package/docs/install-wizard.md +2040 -0
  35. package/docs/native-authority-architecture.md +1 -1
  36. package/docs/permission-modes.md +127 -0
  37. package/docs/prompt-history.md +1 -1
  38. package/docs/readme-reference.md +310 -47
  39. package/docs/render-cache-performance.md +28 -0
  40. package/docs/session-profile-format.md +39 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/yolo-mode.md +11 -10
  44. package/extensions/ask-user-choice.ts +4 -0
  45. package/extensions/ask-user-question.ts +6 -1
  46. package/extensions/child-context.ts +11 -2
  47. package/extensions/child-safety.ts +4 -2
  48. package/extensions/codegraph-tools.ts +18 -5
  49. package/extensions/gentle-agents.ts +718 -87
  50. package/extensions/gentle-ai.ts +991 -263
  51. package/extensions/gentle-herdr-activity.ts +81 -0
  52. package/extensions/gentle-notifications.ts +104 -0
  53. package/extensions/gentle-shell.ts +271 -80
  54. package/extensions/gentle-todo.ts +123 -18
  55. package/extensions/history/index.ts +11 -6
  56. package/extensions/pi-pretty.ts +39 -3
  57. package/extensions/quiet-tools.ts +91 -21
  58. package/extensions/skill-registry.ts +175 -46
  59. package/extensions/startup-banner.ts +11 -2
  60. package/lib/agent-home.ts +10 -1
  61. package/lib/agent-model-resolution.ts +80 -0
  62. package/lib/agent-profiles.ts +13 -3
  63. package/lib/agents-completion-delivery.ts +11 -3
  64. package/lib/agents-messaging.ts +1 -1
  65. package/lib/agents-protocol.ts +40 -1
  66. package/lib/agents-runner.ts +90 -15
  67. package/lib/auto-permission-answers.ts +133 -0
  68. package/lib/auto-permission-classifier.ts +459 -0
  69. package/lib/auto-permission-evidence.ts +88 -0
  70. package/lib/auto-permission-memory.ts +69 -0
  71. package/lib/auto-permission-runtime.ts +484 -0
  72. package/lib/auto-permission-tool-context.ts +61 -0
  73. package/lib/background-jobs-tools.ts +202 -0
  74. package/lib/background-jobs.ts +253 -0
  75. package/lib/background-monitor.ts +154 -0
  76. package/lib/bounded-writer-admission.ts +83 -10
  77. package/lib/builtin-codemode-optout.ts +166 -0
  78. package/lib/card-content-policy.ts +85 -0
  79. package/lib/child-package-injection.ts +62 -0
  80. package/lib/codemode-renderer.ts +14 -4
  81. package/lib/command-palette-catalog.ts +3 -2
  82. package/lib/context-threshold.ts +26 -0
  83. package/lib/destructive-command-guard.ts +104 -28
  84. package/lib/gentle-ai-binary.ts +109 -4
  85. package/lib/gentle-shell-bundled.ts +535 -0
  86. package/lib/gentle-shell-launcher.ts +75 -24
  87. package/lib/gentle-shell-startup-flow.ts +54 -0
  88. package/lib/gentle-shell-startup-update.ts +92 -0
  89. package/lib/gentle-shell-uninstall.ts +840 -0
  90. package/lib/gentle-shell-update-panel.ts +231 -0
  91. package/lib/herdr-activity.ts +183 -0
  92. package/lib/inprocess-reviewer.ts +3 -0
  93. package/lib/jobs-sidebar-state.ts +20 -0
  94. package/lib/jobs-view.ts +161 -0
  95. package/lib/model-price-ratio.ts +83 -0
  96. package/lib/model-routing-authority.ts +6 -1
  97. package/lib/nan-provider.ts +33 -11
  98. package/lib/native-review-cli.ts +194 -73
  99. package/lib/notification-audio-native.ts +162 -0
  100. package/lib/notification-audio-windows.ts +399 -0
  101. package/lib/notification-audio.ts +332 -0
  102. package/lib/notification-customize.ts +241 -0
  103. package/lib/notification-events.ts +113 -0
  104. package/lib/notification-policy.ts +148 -0
  105. package/lib/notification-pulse-client.ts +367 -0
  106. package/lib/notification-pulse-protocol.ts +305 -0
  107. package/lib/notification-pulse-stream.ts +213 -0
  108. package/lib/notification-pulse-worker.ts +66 -0
  109. package/lib/notification-scheduler.ts +173 -0
  110. package/lib/notification-service.ts +202 -0
  111. package/lib/notification-ui.ts +87 -0
  112. package/lib/odd-phase.ts +1 -1
  113. package/lib/orchestrator-alias.ts +20 -0
  114. package/lib/orchestrator-catalog.ts +79 -0
  115. package/lib/orchestrator-consultation.ts +61 -0
  116. package/lib/orchestrator-discovery.ts +49 -0
  117. package/lib/orchestrator-helper-consent.ts +122 -0
  118. package/lib/orchestrator-helper.ts +167 -0
  119. package/lib/orchestrator-message-card.ts +105 -0
  120. package/lib/orchestrator-presence.ts +115 -14
  121. package/lib/orchestrator-scope.ts +80 -0
  122. package/lib/orchestrator-state.ts +107 -0
  123. package/lib/orchestrator-tool-card.ts +280 -0
  124. package/lib/orchestrator-work-search.ts +218 -0
  125. package/lib/orchestrator-work.ts +79 -0
  126. package/lib/permission-mode-policy.ts +59 -0
  127. package/lib/review-candidate-view-owner.ts +8 -6
  128. package/lib/review-candidate-view.ts +3 -2
  129. package/lib/review-compact-contract.ts +2 -2
  130. package/lib/review-host-relay.ts +7 -2
  131. package/lib/review-integration-v2.ts +100 -22
  132. package/lib/review-publication-gate.ts +1 -1
  133. package/lib/review-risk-assessment.ts +77 -51
  134. package/lib/review-sidebar-state.ts +2 -1
  135. package/lib/review-transaction.ts +72 -3
  136. package/lib/runtime-metrics-children.ts +1 -1
  137. package/lib/runtime-metrics-native.ts +3 -2
  138. package/lib/selection-engine.ts +22 -7
  139. package/lib/session-change-capture.ts +5 -4
  140. package/lib/session-changes.ts +27 -22
  141. package/lib/session-profile-binding.ts +83 -0
  142. package/lib/session-profile-freeze.ts +261 -0
  143. package/lib/session-profile-persistence.ts +187 -0
  144. package/lib/session-worktree-registry.ts +74 -8
  145. package/lib/shell-bar.ts +41 -11
  146. package/lib/shell-changes-view.ts +40 -13
  147. package/lib/shell-changes.ts +88 -71
  148. package/lib/shell-prompt.ts +6 -4
  149. package/lib/shell-sidebar-layout.ts +5 -1
  150. package/lib/shell-todo.ts +81 -26
  151. package/lib/shell-usage.ts +4 -3
  152. package/lib/status-timing-diagnostics.ts +201 -0
  153. package/lib/telemetry-trigger.ts +3 -1
  154. package/lib/vim-editor-adapter.ts +27 -5
  155. package/lib/visual-customization-policy.ts +20 -9
  156. package/lib/visual-customize-view.ts +171 -20
  157. package/lib/writer-surfaces.ts +162 -0
  158. package/lib/yolo-session-policy.ts +198 -121
  159. package/package.json +5 -5
  160. package/runtime/child-package-injection.mjs +63 -0
  161. package/runtime/gentle-ai-binary.mjs +109 -4
  162. package/runtime/gentle-shell-bundled.mjs +536 -0
  163. package/runtime/gentle-shell-launcher.mjs +74 -23
  164. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  165. package/runtime/gentle-shell-startup-update.mjs +93 -0
  166. package/runtime/gentle-shell-uninstall.mjs +841 -0
  167. package/runtime/gentle-shell-update-panel.mjs +232 -0
  168. package/runtime/native-review-cli.mjs +191 -70
  169. package/runtime/review-integration-v2.mjs +100 -22
  170. package/runtime/review-risk-assessment.mjs +77 -51
  171. package/runtime/status-timing-diagnostics.mjs +202 -0
  172. package/runtime/telemetry-trigger.mjs +3 -1
  173. package/scripts/bootstrap.cmd +255 -0
  174. package/scripts/bootstrap.sh +168 -0
  175. package/scripts/build-distribution.mjs +157 -0
  176. package/scripts/build-installer-bundles.mjs +136 -0
  177. package/scripts/build-runtime-modules.mjs +7 -0
  178. package/scripts/bundled-install.mjs +1308 -0
  179. package/scripts/bundled-wizard.mjs +399 -0
  180. package/scripts/check-types.mjs +4 -6
  181. package/scripts/gentle-ai-installer.mjs +79 -32
  182. package/scripts/install-gentle-ai.mjs +48 -1
  183. package/scripts/install-tui-mode-setting.mjs +10 -1
  184. package/scripts/install-wizard-preview.mjs +154 -0
  185. package/scripts/installer-downloads.mjs +648 -0
  186. package/scripts/installer-preflight.mjs +387 -0
  187. package/scripts/installer-probes.mjs +564 -0
  188. package/scripts/installer-runner.mjs +1102 -0
  189. package/scripts/installer-server.mjs +881 -0
  190. package/scripts/installer-windows-artifacts.json +15 -0
  191. package/scripts/installer-windows.mjs +752 -0
  192. package/scripts/main-channel.mjs +452 -0
  193. package/scripts/npm-pack-result.mjs +31 -0
  194. package/scripts/pi-update.mjs +223 -0
  195. package/scripts/render-cache-performance-worker.mjs +205 -0
  196. package/scripts/render-cache-performance.mjs +115 -0
  197. package/scripts/run-test-suite.mjs +22 -8
  198. package/scripts/test-installer-acceptance.sh +161 -0
  199. package/scripts/test-packed-runner.mjs +23 -5
  200. package/scripts/types-baseline.json +2 -92
  201. package/scripts/verify-package-files.mjs +82 -18
  202. package/skills/gentle-ai/SKILL.md +12 -11
  203. package/tests/agent-home.test.ts +56 -3
  204. package/tests/agent-model-resolution.test.ts +113 -0
  205. package/tests/agent-profiles.test.ts +12 -0
  206. package/tests/agent-risk-escalation.test.ts +123 -0
  207. package/tests/agents-completion-delivery.test.ts +17 -0
  208. package/tests/agents-protocol.test.ts +57 -0
  209. package/tests/agents-queries.test.ts +9 -9
  210. package/tests/agents-runner-process.test.ts +31 -0
  211. package/tests/agents-runner.test.ts +89 -4
  212. package/tests/append-system-prompt-route.test.ts +18 -1
  213. package/tests/auto-mode-human-answers.test.ts +68 -0
  214. package/tests/auto-mode-invariants.test.ts +276 -0
  215. package/tests/auto-mode-sdk.test.ts +84 -0
  216. package/tests/auto-mode.test.ts +49 -0
  217. package/tests/auto-permission-answers.test.ts +170 -0
  218. package/tests/auto-permission-classifier.test.ts +634 -0
  219. package/tests/auto-permission-compound.test.ts +24 -0
  220. package/tests/auto-permission-evidence.test.ts +88 -0
  221. package/tests/auto-permission-memory.test.ts +46 -0
  222. package/tests/auto-permission-public-regressions.test.ts +111 -0
  223. package/tests/auto-permission-runtime.test.ts +454 -0
  224. package/tests/autonomous-guard.test.ts +86 -16
  225. package/tests/background-cache-warming.test.ts +2 -1
  226. package/tests/background-jobs.test.ts +227 -0
  227. package/tests/background-monitor.test.ts +195 -0
  228. package/tests/background-subagents-default-mode.test.ts +28 -5
  229. package/tests/bounded-stop-contract.test.ts +85 -0
  230. package/tests/bounded-writer-admission.test.ts +52 -1
  231. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  232. package/tests/bridge-wake-rendering.test.ts +56 -0
  233. package/tests/build-distribution.test.ts +269 -0
  234. package/tests/builtin-codemode-optout.test.ts +242 -0
  235. package/tests/bundled-install.test.ts +1319 -0
  236. package/tests/bundled-wizard.test.ts +505 -0
  237. package/tests/card-content-policy.test.ts +68 -0
  238. package/tests/card-style-policy.test.ts +34 -2
  239. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  240. package/tests/child-package-entrypoints.test.ts +65 -0
  241. package/tests/child-package-injection.test.ts +74 -0
  242. package/tests/child-safety.test.ts +12 -0
  243. package/tests/child-web-package.test.ts +224 -0
  244. package/tests/codegraph-tools.test.ts +35 -0
  245. package/tests/codemode-rendering.test.ts +107 -2
  246. package/tests/command-palette.test.ts +5 -5
  247. package/tests/context-threshold.test.ts +31 -0
  248. package/tests/delegate-for-reason-contract.test.ts +327 -0
  249. package/tests/delegated-key-learnings-contract.test.ts +3 -2
  250. package/tests/devbinary/native-review-parity.devtest.ts +1 -1
  251. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  252. package/tests/engram-safety.test.ts +316 -0
  253. package/tests/generic-agent-tools.test.ts +16 -1
  254. package/tests/gentle-agents.test.ts +1895 -76
  255. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  256. package/tests/gentle-ai-binary.test.ts +19 -2
  257. package/tests/gentle-ai-child-guards.test.ts +202 -0
  258. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  259. package/tests/gentle-ai-installer.test.ts +548 -58
  260. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  261. package/tests/gentle-ai-renderer.test.ts +6 -6
  262. package/tests/gentle-ai.test.ts +999 -31
  263. package/tests/gentle-herdr-activity.test.ts +100 -0
  264. package/tests/gentle-notifications.test.ts +427 -0
  265. package/tests/gentle-shell-bin.test.ts +263 -19
  266. package/tests/gentle-shell-bundled.test.ts +1036 -0
  267. package/tests/gentle-shell-launcher.test.ts +251 -25
  268. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  269. package/tests/gentle-shell-startup-update.test.ts +85 -0
  270. package/tests/gentle-shell-uninstall.test.ts +978 -0
  271. package/tests/gentle-shell-update-panel.test.ts +158 -0
  272. package/tests/gentle-shell.test.ts +611 -100
  273. package/tests/gentle-todo.test.ts +317 -14
  274. package/tests/herdr-activity.test.ts +243 -0
  275. package/tests/history-agent-home.test.ts +81 -0
  276. package/tests/history-child-guard.test.ts +70 -0
  277. package/tests/inprocess-reviewer.test.ts +20 -0
  278. package/tests/install-gentle-ai-failure.test.ts +69 -0
  279. package/tests/install-tui-mode-setting.test.ts +15 -0
  280. package/tests/install-wizard.test.ts +1223 -0
  281. package/tests/installer-bundles.test.ts +122 -0
  282. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  283. package/tests/installer-preflight.test.ts +750 -0
  284. package/tests/installer-probes.test.ts +1106 -0
  285. package/tests/installer-runner.test.ts +2159 -0
  286. package/tests/installer-server.test.ts +1567 -0
  287. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  288. package/tests/jobs-view.test.ts +160 -0
  289. package/tests/main-channel-upgrade.test.ts +395 -0
  290. package/tests/main-channel.test.ts +344 -0
  291. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  292. package/tests/model-price-ratio.test.ts +116 -0
  293. package/tests/model-profiles-preservation.test.ts +153 -0
  294. package/tests/model-routing-package.test.ts +10 -0
  295. package/tests/nan-provider-runtime.test.ts +156 -0
  296. package/tests/nan-provider.test.ts +28 -18
  297. package/tests/native-binary-gate.test.ts +2 -0
  298. package/tests/native-review-capability-contract.test.ts +7 -1
  299. package/tests/native-review-cli.test.ts +236 -1
  300. package/tests/native-review-consent.test.ts +2 -2
  301. package/tests/native-review-parity-runtime.test.ts +1 -1
  302. package/tests/native-review-parity.test.ts +12 -48
  303. package/tests/native-risk-evidence-parity.test.ts +21 -0
  304. package/tests/no-self-pause-contract.test.ts +87 -0
  305. package/tests/notification-audio-native.test.ts +281 -0
  306. package/tests/notification-audio-windows.test.ts +581 -0
  307. package/tests/notification-audio.test.ts +548 -0
  308. package/tests/notification-customize.test.ts +655 -0
  309. package/tests/notification-events.test.ts +115 -0
  310. package/tests/notification-policy.test.ts +201 -0
  311. package/tests/notification-pulse-client.test.ts +356 -0
  312. package/tests/notification-pulse-protocol.test.ts +213 -0
  313. package/tests/notification-pulse-stream.test.ts +320 -0
  314. package/tests/notification-scheduler.test.ts +258 -0
  315. package/tests/notification-ui.test.ts +157 -0
  316. package/tests/npm-pack-result.test.ts +41 -0
  317. package/tests/odd-integration.test.ts +14 -5
  318. package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
  319. package/tests/odd-routing-contract.test.ts +80 -23
  320. package/tests/odd-runtime-delegation-gate.test.ts +4 -1
  321. package/tests/orchestrator-budget.test.ts +29 -6
  322. package/tests/orchestrator-catalog.test.ts +146 -0
  323. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  324. package/tests/orchestrator-consultation.test.ts +111 -0
  325. package/tests/orchestrator-discovery.test.ts +241 -0
  326. package/tests/orchestrator-helper-consent.test.ts +171 -0
  327. package/tests/orchestrator-helper.test.ts +297 -0
  328. package/tests/orchestrator-message-card.test.ts +190 -0
  329. package/tests/orchestrator-presence.test.ts +23 -0
  330. package/tests/orchestrator-rdd-ownership.test.ts +6 -5
  331. package/tests/orchestrator-scope.test.ts +107 -0
  332. package/tests/orchestrator-state.test.ts +114 -0
  333. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  334. package/tests/orchestrator-tool-card.test.ts +398 -0
  335. package/tests/orchestrator-work-search.test.ts +318 -0
  336. package/tests/orchestrator-work.test.ts +147 -0
  337. package/tests/package-manifest.test.ts +105 -21
  338. package/tests/packed-runner-owned-path.test.ts +35 -1
  339. package/tests/parallel-reason-contract.test.ts +21 -0
  340. package/tests/parallel-writer-admission.test.ts +210 -0
  341. package/tests/permission-mode-policy.test.ts +194 -0
  342. package/tests/persona-single-channel.test.ts +4 -12
  343. package/tests/pi-pretty.test.ts +56 -0
  344. package/tests/pi-update-launcher.test.ts +87 -0
  345. package/tests/pi-update.test.ts +186 -0
  346. package/tests/provider-defect-handoff.test.ts +4 -3
  347. package/tests/quiet-tool-rendering.test.ts +160 -3
  348. package/tests/rdd-aware-verification-contract.test.ts +29 -41
  349. package/tests/rdd-status-line.test.ts +52 -0
  350. package/tests/render-cache-performance.test.ts +48 -0
  351. package/tests/review-agent-end-preflight.test.ts +62 -10
  352. package/tests/review-candidate-view.test.ts +101 -12
  353. package/tests/review-contract-prompt.test.ts +33 -4
  354. package/tests/review-controller-native-recovery.test.ts +327 -17
  355. package/tests/review-controller-native-routing.test.ts +255 -1
  356. package/tests/review-controller-workspace-root.test.ts +43 -2
  357. package/tests/review-controller.test.ts +2 -2
  358. package/tests/review-correction-lifecycle.test.ts +2 -1
  359. package/tests/review-gate.test.ts +7 -4
  360. package/tests/review-graph-schema.test.ts +2 -2
  361. package/tests/review-host-relay-routing.test.ts +5 -3
  362. package/tests/review-host-relay.test.ts +49 -2
  363. package/tests/review-integration-v2-forward.test.ts +21 -1
  364. package/tests/review-json-arguments.test.ts +167 -0
  365. package/tests/review-last-event-closure.test.ts +157 -8
  366. package/tests/review-ledger-contract.test.ts +5 -4
  367. package/tests/review-policy-judgment-day.test.ts +1 -1
  368. package/tests/review-policy-ordinary.test.ts +2 -2
  369. package/tests/review-relay-transport-agent.test.ts +66 -1
  370. package/tests/review-risk-assessment.test.ts +109 -33
  371. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  372. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  373. package/tests/review-test-fixtures.ts +7 -1
  374. package/tests/review-transaction.test.ts +18 -1
  375. package/tests/run-test-suite.test.ts +83 -5
  376. package/tests/runtime-harness.mjs +42 -15
  377. package/tests/runtime-metrics-extension.test.ts +1 -1
  378. package/tests/runtime-metrics-native.test.ts +31 -1
  379. package/tests/selection-engine.test.ts +65 -5
  380. package/tests/session-change-capture.test.ts +10 -10
  381. package/tests/session-changes-shell.test.ts +242 -48
  382. package/tests/session-changes.test.ts +17 -24
  383. package/tests/session-profile-binding.test.ts +163 -0
  384. package/tests/session-profile-freeze.test.ts +394 -0
  385. package/tests/session-profile-persistence.test.ts +334 -0
  386. package/tests/session-subject-routing-contract.test.ts +34 -0
  387. package/tests/session-worktree-registry.test.ts +57 -2
  388. package/tests/shell-bar.test.ts +40 -0
  389. package/tests/shell-changes-view.test.ts +85 -34
  390. package/tests/shell-changes.test.ts +79 -62
  391. package/tests/shell-render-cache-runtime.test.ts +69 -0
  392. package/tests/shell-render-cache.test.ts +129 -0
  393. package/tests/shell-sidebar-layout.test.ts +27 -2
  394. package/tests/shell-todo.test.ts +132 -11
  395. package/tests/shell-usage-view.test.ts +1 -1
  396. package/tests/shell-usage.test.ts +35 -0
  397. package/tests/short-check-routing-contract.test.ts +49 -0
  398. package/tests/skill-registry-watcher.test.ts +119 -0
  399. package/tests/skill-registry.test.ts +311 -5
  400. package/tests/startup-banner.test.ts +117 -3
  401. package/tests/startup-home-repo.test.ts +170 -0
  402. package/tests/status-timing-diagnostics.test.ts +283 -0
  403. package/tests/support/orchestrator-modules.ts +19 -0
  404. package/tests/task-size-routing-contract.test.ts +241 -0
  405. package/tests/telemetry-trigger.test.ts +27 -1
  406. package/tests/test-strength-contract.test.ts +124 -0
  407. package/tests/verify-agent-contract.test.ts +90 -0
  408. package/tests/verify-first-launch-contract.test.ts +139 -0
  409. package/tests/verify-package-files.test.ts +56 -0
  410. package/tests/vim-editor-adapter.test.ts +47 -21
  411. package/tests/visual-customization-policy.test.ts +32 -3
  412. package/tests/visual-customize-view.test.ts +288 -10
  413. package/tests/writer-edit-surface-scope.test.ts +59 -2
  414. package/tests/writer-surfaces.test.ts +199 -0
  415. package/tests/yolo-customize.test.ts +18 -3
  416. package/tests/yolo-mode-runtime.test.ts +37 -10
  417. package/tests/yolo-mode.test.ts +38 -5
  418. package/tests/yolo-session-policy.test.ts +1 -1
  419. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  420. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  421. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  422. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  423. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  424. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  425. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -10,11 +10,14 @@ import {
10
10
  closeSync,
11
11
  constants as fsConstants,
12
12
  existsSync,
13
+ fstatSync,
14
+ lstatSync,
13
15
  mkdirSync,
14
16
  mkdtempSync,
15
17
  openSync,
16
18
  readdirSync,
17
19
  readFileSync,
20
+ readSync,
18
21
  realpathSync,
19
22
  renameSync,
20
23
  rmSync,
@@ -24,6 +27,7 @@ import {
24
27
  import { constants as osConstants, homedir, tmpdir } from "node:os";
25
28
  import { delimiter, dirname, join, resolve as resolvePath } from "node:path";
26
29
  import { spawn, spawnSync } from "node:child_process";
30
+ import { createHash } from "node:crypto";
27
31
  import { fileURLToPath } from "node:url";
28
32
  import {
29
33
  buildPiInvocation,
@@ -54,6 +58,33 @@ import {
54
58
  restoreJsonField,
55
59
  shellQuote,
56
60
  } from "../runtime/gentle-shell-launcher.mjs";
61
+ import {
62
+ BUNDLED_VERIFY_ENV,
63
+ bundledEngramSchema,
64
+ classifyEngramStartup,
65
+ ENGRAM_COMPANION_SOURCE,
66
+ ENGRAM_PROVISION_FAILED_NOTICE,
67
+ ENGRAM_RECOVERY_NOTICE,
68
+ engramHeaderAllows,
69
+ engramProbeAllowed,
70
+ engramProvenanceMatches,
71
+ engramProvenancePath,
72
+ parseEngramProvenance,
73
+ piVersionOrHelpRequested,
74
+ probeEngramEndpoint,
75
+ sqliteHeaderUserVersion,
76
+ supportsEngramStartupPolicy,
77
+ bundledChildEnv,
78
+ bundledPi,
79
+ bundledSetupEnv,
80
+ bundledUpdateNext,
81
+ bundledUpdateNotice,
82
+ bundledUpdatePlan,
83
+ nodeBundledFs,
84
+ parseBundledUpgradeArgs,
85
+ resolveBundledInstall,
86
+ withBundledNpmCommand,
87
+ } from "../runtime/gentle-shell-bundled.mjs";
57
88
  import {
58
89
  parseResumeHandoff,
59
90
  planResumeHint,
@@ -61,8 +92,12 @@ import {
61
92
  RESUME_HANDOFF_ENV,
62
93
  RESUME_HANDOFF_FILE,
63
94
  } from "../runtime/gentle-shell-resume-hint.mjs";
64
- import { GENTLE_AI_VERSION, gentleAiBinaryPath, PackageLocalGentleAiBinaryMissingError } from "../runtime/gentle-ai-binary.mjs";
95
+ import { bundledGentleAiPaths, GENTLE_AI_VERSION, gentleAiBinaryPath, gentleAiIsolationEnv, PackageLocalGentleAiBinaryMissingError } from "../runtime/gentle-ai-binary.mjs";
65
96
  import { DEFAULT_THEME_NAME, installIsolatedTuiModeSetting } from "../scripts/install-tui-mode-setting.mjs";
97
+ import { artifactFor, engramPinVersion } from "../scripts/installer-downloads.mjs";
98
+ import { shouldCheckStartupUpdate, findStartupUpdate } from "../runtime/gentle-shell-startup-update.mjs";
99
+ import { promptStartupUpdate } from "../runtime/gentle-shell-update-panel.mjs";
100
+ import { applyStartupChoice, omitStartupVersion, resolveStartupRelaunch } from "../runtime/gentle-shell-startup-flow.mjs";
66
101
 
67
102
  const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
68
103
 
@@ -128,6 +163,213 @@ function signalExitCode(signal) {
128
163
  return 128 + (typeof number === "number" ? number : 0);
129
164
  }
130
165
 
166
+ // A bundled install (S8): this package inside <prefix>/versions/<id> that
167
+ // `current` names (lib/gentle-shell-bundled.ts decides from these reads).
168
+ const bundledFs = nodeBundledFs;
169
+
170
+ // Set in a bundled install: the environment every child (Pi, setup, the version
171
+ // probe) starts from instead of process.env (the user's own plus Gentle Shell's
172
+ // variables), and the folders setup puts ahead of the user's PATH.
173
+ let bundledEnv;
174
+ let bundledSetupDirs;
175
+ // The bundled install and its Pi cli, for bundledProcessEnv and setup's Engram.
176
+ let bundledInstall;
177
+ let bundledCli;
178
+ let bundledMemory;
179
+ // The Shell-owned guard ENGRAM_BIN names when memory is disabled (bin/engram-guard).
180
+ const ENGRAM_GUARD = join(packageRoot, "bin", "engram-guard");
181
+
182
+ // The pinned core only as ensureEngram published it: its runtime marker for
183
+ // this release's archive and a regular file. Never a link or a PATH engram.
184
+ function verifiedBundledCore() {
185
+ const core = bundledGentleAiPaths(bundledInstall.root, process.platform).engram;
186
+ try {
187
+ const marker = readFileSync(join(dirname(core), ".gentle-shell-runtime"), "utf8");
188
+ if (marker === `${artifactFor("engram", process.platform, process.arch).url}\n` && lstatSync(core).isFile()) return core;
189
+ } catch {
190
+ // Missing, unmarked, or an unsupported target: no verified core.
191
+ }
192
+ return undefined;
193
+ }
194
+
195
+ function sha256File(path) {
196
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
197
+ }
198
+
199
+ function readSmallRegularFile(path, limit) {
200
+ try {
201
+ const info = lstatSync(path);
202
+ return info.isFile() && info.size <= limit ? readFileSync(path, "utf8") : undefined;
203
+ } catch {
204
+ return undefined;
205
+ }
206
+ }
207
+
208
+ // The database's raw SQLite header, read as a plain file (never through
209
+ // SQLite): a regular, unlinked engram.db whose opened identity is the one seen.
210
+ function readSqliteUserVersion(path) {
211
+ let fd;
212
+ try {
213
+ const seen = lstatSync(path);
214
+ if (!seen.isFile()) return undefined;
215
+ fd = openSync(path, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
216
+ const opened = fstatSync(fd);
217
+ if (opened.ino !== seen.ino || opened.dev !== seen.dev) return undefined;
218
+ const header = Buffer.alloc(100);
219
+ return readSync(fd, header, 0, 100, 0) === 100 ? sqliteHeaderUserVersion(header, opened.size) : undefined;
220
+ } catch {
221
+ return undefined;
222
+ } finally {
223
+ if (fd !== undefined) closeSync(fd);
224
+ }
225
+ }
226
+
227
+ // The default data directory without opening it: presence, canonical path,
228
+ // the .instance-id file and the database header. A link is never followed.
229
+ function observeEngramStore() {
230
+ const dir = join(homedir(), ".engram");
231
+ let dataDir;
232
+ try {
233
+ if (!lstatSync(dir).isDirectory()) return { store: "unknown" };
234
+ dataDir = realpathSync(dir);
235
+ } catch (error) {
236
+ return { store: error.code === "ENOENT" ? "absent" : "unknown" };
237
+ }
238
+ return { store: "present", dir, dataDir, instanceId: readSmallRegularFile(join(dir, ".instance-id"), 128)?.trim() };
239
+ }
240
+
241
+ // Only the identity files Engram's `instance-id` writes: a store Shell
242
+ // provisioned whose session has not created its database yet.
243
+ function identityOnlyStore(dir) {
244
+ try {
245
+ return readdirSync(dir).every((name) => name === ".instance-id" || name === ".instance-id.lock" || name.startsWith(".instance-id-"));
246
+ } catch {
247
+ return false;
248
+ }
249
+ }
250
+
251
+ // Shell's own store: the record Shell wrote when it provisioned this exact
252
+ // directory's identity, the same core digest, and a database header no newer
253
+ // than the bundled schema (or no database yet).
254
+ function ownEngramStore(observed, core) {
255
+ let record;
256
+ try {
257
+ record = parseEngramProvenance(readJsonIfExists(engramProvenancePath(bundledInstall.root, process.platform)));
258
+ } catch {
259
+ return undefined;
260
+ }
261
+ if (record === undefined || observed.store !== "present" || core === undefined) return undefined;
262
+ if (!engramProvenanceMatches(record, { dataDir: observed.dataDir, instanceId: observed.instanceId, coreSha256: sha256File(core) })) return undefined;
263
+ const headerAllows = engramHeaderAllows(readSqliteUserVersion(join(observed.dir, "engram.db")), bundledEngramSchema(engramPinVersion));
264
+ return headerAllows || identityOnlyStore(observed.dir) ? { instanceId: record.instanceId } : undefined;
265
+ }
266
+
267
+ // Evidence only: file metadata, the raw header and a bounded read-only probe
268
+ // of the default endpoint. No Engram executable, SQLite, identity repair, or
269
+ // contact with an explicitly selected endpoint.
270
+ async function classifyBundledMemory() {
271
+ const core = verifiedBundledCore();
272
+ const decision = { env: process.env, core: core !== undefined, coreVersion: engramPinVersion };
273
+ if (!engramProbeAllowed(process.env)) return classifyEngramStartup({ ...decision, store: "unknown", endpoint: { kind: "unknown" } });
274
+ const observed = observeEngramStore();
275
+ const endpoint = await probeEngramEndpoint();
276
+ return classifyEngramStartup({ ...decision, store: observed.store, endpoint, provenance: ownEngramStore(observed, core) });
277
+ }
278
+
279
+ // Verified with the published Engram v3.4.0 darwin-arm64 binary in an isolated
280
+ // HOME: `instance-id` creates only `.instance-id.lock` and `.instance-id`,
281
+ // without a database. Keep update checks suppressed and GitHub tokens removed
282
+ // for this provisioning invocation. The timeout stays below the identity lock
283
+ // wait: a held lock means a concurrent creator, so memory stays off that launch.
284
+ const ENGRAM_PROVISION_TIMEOUT_MS = 15000;
285
+
286
+ // A fresh launch's store identity, provisioned by Shell itself with the verified
287
+ // core before Pi starts, then recorded. False when any step fails.
288
+ function provisionEngramStore() {
289
+ const core = verifiedBundledCore();
290
+ if (core === undefined || observeEngramStore().store !== "absent") return false;
291
+ let target;
292
+ try {
293
+ target = join(realpathSync(homedir()), ".engram");
294
+ } catch {
295
+ return false;
296
+ }
297
+ // The provider's own environment, except for this one invocation: no update
298
+ // check and no GitHub tokens. ENGRAM_DATA_DIR is never set here.
299
+ const env = { ...process.env, ENGRAM_NO_UPDATE_CHECK: "1" };
300
+ delete env.GH_TOKEN;
301
+ delete env.GITHUB_TOKEN;
302
+ const result = spawnSync(core, ["instance-id"], { env, encoding: "utf8", timeout: ENGRAM_PROVISION_TIMEOUT_MS, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
303
+ const observed = observeEngramStore();
304
+ if (result.status !== 0 || observed.store !== "present" || observed.dataDir !== target || !/^[a-f0-9]{32}$/.test(observed.instanceId ?? "") ||
305
+ String(result.stdout).trim() !== observed.instanceId) return false;
306
+ return writeEngramProvenance({ schema: 1, dataDir: target, instanceId: observed.instanceId, coreSha256: sha256File(core) });
307
+ }
308
+
309
+ function writeEngramProvenance(record) {
310
+ try {
311
+ const path = engramProvenancePath(bundledInstall.root, process.platform);
312
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
313
+ const temporary = `${path}.${process.pid}.tmp`;
314
+ writeFileSync(temporary, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600, flag: "wx" });
315
+ renameSync(temporary, path);
316
+ return true;
317
+ } catch (error) {
318
+ process.stderr.write(`gentle-shell: could not record Engram provenance (${error.message})\n`);
319
+ return false;
320
+ }
321
+ }
322
+
323
+ // Use Pi's supported configuration-only install command, never an alternate
324
+ // extension loader. Verify advertised enforcement before trusting the policy.
325
+ async function ensureEngramCompanion(home, runtime, stdio, timeoutMs) {
326
+ const metadata = join(home.dir, "npm", "node_modules", "gentle-engram", "package.json");
327
+ let packages;
328
+ try {
329
+ packages = JSON.parse(readJsonIfExists(join(home.dir, "settings.json")) ?? "{}").packages ?? [];
330
+ } catch {
331
+ return { ok: false, exitCode: 1, message: "gentle-shell: cannot verify Engram startupPolicy from malformed settings; repair settings and recheck" };
332
+ }
333
+ if (!Array.isArray(packages)) return { ok: false, exitCode: 1, message: "gentle-shell: cannot verify Engram startupPolicy from non-array packages" };
334
+ let pinned = false;
335
+ let local = false;
336
+ let npmSelected = false;
337
+ for (const entry of packages) {
338
+ const source = typeof entry === "string" ? entry : entry?.source;
339
+ if (source === ENGRAM_COMPANION_SOURCE) pinned = true;
340
+ if (typeof source === "string" && /^npm:gentle-engram(?:@|$)/.test(source)) npmSelected = true;
341
+ if (typeof source !== "string" || source.startsWith("npm:")) continue;
342
+ // Preserve an explicit local companion selection rather than overwrite it.
343
+ const selected = resolvePath(home.dir, source);
344
+ if (readPackageName(selected) !== "gentle-engram") continue;
345
+ if (!supportsEngramStartupPolicy(readJsonIfExists(join(selected, "package.json")))) {
346
+ return { ok: false, exitCode: 1, message: `gentle-shell: selected companion ${source} lacks engram.startupPolicy=1; update it and recheck (selection preserved)` };
347
+ }
348
+ local = true;
349
+ }
350
+ if (local && !npmSelected) return { ok: true };
351
+ if (pinned && supportsEngramStartupPolicy(readJsonIfExists(metadata))) return { ok: true };
352
+ process.stderr.write(`gentle-shell: installing policy-capable companion ${ENGRAM_COMPANION_SOURCE} through Pi\n`);
353
+ const result = await spawnAndWait(runtime.command, [...runtime.args, "install", ENGRAM_COMPANION_SOURCE], buildSetupEnv(home, runtime), stdio, timeoutMs);
354
+ let declared = false;
355
+ try {
356
+ declared = JSON.parse(readJsonIfExists(join(home.dir, "settings.json")) ?? "{}").packages?.some((entry) =>
357
+ (typeof entry === "string" ? entry : entry?.source) === ENGRAM_COMPANION_SOURCE) === true;
358
+ } catch { /* Missing or malformed readback cannot establish enforcement. */ }
359
+ if (!result.ok || result.timedOut || !declared || !supportsEngramStartupPolicy(readJsonIfExists(metadata))) {
360
+ return { ok: false, exitCode: 1, message: "gentle-shell: companion installation did not establish engram.startupPolicy=1; the Engram guard still keeps unverified stores closed. Repair gentle-engram, then restart Shell." };
361
+ }
362
+ return { ok: true };
363
+ }
364
+
365
+ // The bundled children's environment (bundledChildEnv), with the pinned Engram
366
+ // only once it is published: computed again right before Pi starts, so an
367
+ // Engram the automatic setup just published is used in the same launch.
368
+ function bundledProcessEnv() {
369
+ return bundledChildEnv(process.env, { node: process.execPath, cli: bundledCli, prefix: bundledInstall.root, engram: verifiedBundledCore(),
370
+ guard: ENGRAM_GUARD, memory: bundledMemory });
371
+ }
372
+
131
373
  function ownPackageVersion() {
132
374
  const packageJson = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
133
375
  return packageJson.version;
@@ -550,15 +792,50 @@ function ensurePackageLocalGentleAi(binaryPath, pinnedVersion, stdio) {
550
792
  // home, and the resolved pi runtime's directory is prepended to PATH so
551
793
  // gentle-ai's (or pi's own) preflight finds `pi` even when it is bundled or
552
794
  // given through GENTLE_SHELL_PI.
553
- function buildSetupEnv(home, runtime) {
795
+ function buildSetupEnv(home, runtime, piLauncherDir) {
796
+ // Bundled: our node, then the version's `pi`, ahead of the user's PATH, and
797
+ // Gentle AI's state, persona and Engram inside the prefix (S9).
798
+ if (bundledEnv) {
799
+ const env = bundledSetupEnv({ ...bundledEnv, PI_CODING_AGENT_DIR: home.dir, GENTLE_PI_AGENT_HOME: home.dir }, { dirs: bundledSetupDirs, platform: process.platform });
800
+ return gentleAiIsolationEnv(env, { packageRoot });
801
+ }
554
802
  return {
555
803
  ...process.env,
556
804
  PI_CODING_AGENT_DIR: home.dir,
557
805
  GENTLE_PI_AGENT_HOME: home.dir,
558
- PATH: `${dirname(runtime.command)}${delimiter}${process.env.PATH ?? ""}`,
806
+ PATH: [...(piLauncherDir ? [piLauncherDir] : []), dirname(runtime.command), process.env.PATH ?? ""].join(delimiter),
559
807
  };
560
808
  }
561
809
 
810
+ // Gentle AI requires a `pi` on its PATH and runs `pi install npm:gentle-pi`
811
+ // with it. A Pi gentle-shell resolved elsewhere (the adjacent package, run
812
+ // as `node <cli>`, or GENTLE_SHELL_PI) is not that `pi`, so a first launch
813
+ // from a terminal without one failed every time. This writes a private,
814
+ // temporary `pi` that runs exactly the resolved runtime; null when the
815
+ // runtime already is the `pi` on PATH (or for a bundled install, whose
816
+ // setup PATH already starts with the version's node_modules/.bin).
817
+ function writeSetupPiLauncher(runtime) {
818
+ if (bundledEnv || runtime.kind === "path") return null;
819
+ let dir;
820
+ try {
821
+ dir = mkdtempSync(join(tmpdir(), "gentle-shell-pi-"));
822
+ } catch {
823
+ // No usable temp folder: setup runs as it did before, without this `pi`.
824
+ return null;
825
+ }
826
+ if (process.platform === "win32") {
827
+ // Batch expands % even inside quotes; double it so a path stays literal.
828
+ const quote = (value) => `"${value.replaceAll("%", "%%")}"`;
829
+ // A batch runtime needs `call`, or control never returns for `exit /b`.
830
+ const call = /\.(?:cmd|bat)$/i.test(runtime.command) ? "call " : "";
831
+ writeFileSync(join(dir, "pi.cmd"), `@echo off\r\n${call}${[runtime.command, ...runtime.args].map(quote).join(" ")} %*\r\nexit /b %errorlevel%\r\n`);
832
+ } else {
833
+ const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
834
+ writeFileSync(join(dir, "pi"), `#!/bin/sh\nexec ${[runtime.command, ...runtime.args].map(quote).join(" ")} "$@"\n`, { mode: 0o700 });
835
+ }
836
+ return dir;
837
+ }
838
+
562
839
  // The shared Pi persona file gentle-ai writes on every install, regardless
563
840
  // of the target home: its own PiPersonaConfigPath always resolves against
564
841
  // the OS home, never PI_CODING_AGENT_DIR (gentle-ai internal/components/persona/inject.go),
@@ -570,6 +847,29 @@ function sharedPersonaPath() {
570
847
  return join(homedir(), ".pi", "gentle-ai", "persona.json");
571
848
  }
572
849
 
850
+ // The persona files a setup run may rewrite. Bundled (S9): the prefix's own,
851
+ // which a Gentle AI with GENTLE_AI_PI_CONFIG_DIR (gentle-ai #5462) writes, and
852
+ // still the user's, which a pinned Gentle AI without it writes; restoring an
853
+ // unchanged file does nothing.
854
+ function setupPersonaPaths() {
855
+ if (!bundledInstall) return [sharedPersonaPath()];
856
+ return [join(bundledGentleAiPaths(bundledInstall.root, process.platform).piConfig, "persona.json"), sharedPersonaPath()];
857
+ }
858
+
859
+ // Bundled setup (S9): the pinned Engram GENTLE_AI_ENGRAM_BIN names, published
860
+ // once in runtime/ (verified, never brew or the user's PATH). A failure stops
861
+ // setup rather than let Gentle AI acquire another Engram.
862
+ async function ensureBundledEngram() {
863
+ const { ensureEngram, prefixLayout } = await import("../scripts/bundled-install.mjs");
864
+ const layout = prefixLayout({ platform: process.platform, env: process.env, home: homedir(), root: bundledInstall.root });
865
+ try {
866
+ await ensureEngram({ layout, platform: process.platform, arch: process.arch, env: process.env });
867
+ return { ok: true };
868
+ } catch (error) {
869
+ return { ok: false, exitCode: 1, message: `gentle-shell: could not install the pinned Engram in ${layout.engramDir} (${error.message})` };
870
+ }
871
+ }
872
+
573
873
  // Records `path`'s current state before a child process that might rewrite
574
874
  // it runs: whether it exists, and if so its exact bytes and mode. Returns
575
875
  // `{ path, existed: false }` for a missing file so restoreFile below knows
@@ -818,10 +1118,24 @@ async function runSetupFlow(home, runtime, { dryRun, stdio, timeoutMs }) {
818
1118
 
819
1119
  process.stderr.write(`gentle-shell: provisioning ${home.dir} with the gentle-ai companion packages\n`);
820
1120
 
1121
+ if (bundledInstall && !dryRun) {
1122
+ const engram = await ensureBundledEngram();
1123
+ if (!engram.ok) return engram;
1124
+ }
1125
+ // The memory decision never skips setup: it only selects the Engram
1126
+ // environment (core or guard, policy, ENGRAM_URL) setup's children and the
1127
+ // launched Pi get. Gentle AI's `install --agent pi` and `pi-engram init` open
1128
+ // no store. Reclassified now that the verified core may just be published.
1129
+ if (bundledInstall) {
1130
+ bundledMemory = await classifyBundledMemory();
1131
+ bundledEnv = bundledProcessEnv();
1132
+ if (bundledMemory.notice) process.stderr.write(`gentle-shell: ${bundledMemory.notice}\n`);
1133
+ }
1134
+
821
1135
  const setupArgs = ["install", "--agent", "pi", "--scope", "global", ...(dryRun ? ["--dry-run"] : [])];
822
- const env = buildSetupEnv(home, runtime);
823
- const personaPath = sharedPersonaPath();
824
- const personaSnapshot = safely("snapshot your Pi persona file", personaPath, undefined, () => snapshotFile(personaPath));
1136
+ const piLauncherDir = writeSetupPiLauncher(runtime);
1137
+ const env = buildSetupEnv(home, runtime, piLauncherDir);
1138
+ const personaSnapshots = setupPersonaPaths().map((path) => [path, safely("snapshot your Pi persona file", path, undefined, () => snapshotFile(path))]);
825
1139
  const statePath = sharedGentleAiStatePath();
826
1140
  const originalStateText = safely("read your Gentle AI state file", statePath, undefined, () => readParsableJsonText(statePath));
827
1141
  // Theme restore (never for --link — that home is the user's own
@@ -838,8 +1152,11 @@ async function runSetupFlow(home, runtime, { dryRun, stdio, timeoutMs }) {
838
1152
  try {
839
1153
  installResult = await spawnAndWait(binaryPath, setupArgs, env, stdio, timeoutMs);
840
1154
  } finally {
841
- if (personaSnapshot !== undefined && safely("restore your Pi persona file", personaPath, false, () => restoreFile(personaSnapshot))) {
842
- process.stderr.write(`gentle-shell: kept your Pi persona unchanged (gentle-ai rewrote ${personaPath}; tracked upstream)\n`);
1155
+ if (piLauncherDir) rmSync(piLauncherDir, { recursive: true, force: true });
1156
+ for (const [personaPath, personaSnapshot] of personaSnapshots) {
1157
+ if (personaSnapshot !== undefined && safely("restore your Pi persona file", personaPath, false, () => restoreFile(personaSnapshot))) {
1158
+ process.stderr.write(`gentle-shell: kept your Pi persona unchanged (gentle-ai rewrote ${personaPath}; tracked upstream)\n`);
1159
+ }
843
1160
  }
844
1161
  if (safely("restore your Gentle AI managed-asset record", statePath, false, () => restoreManagedAssetDigestField(statePath, originalStateText))) {
845
1162
  process.stderr.write(
@@ -861,7 +1178,63 @@ async function runSetupFlow(home, runtime, { dryRun, stdio, timeoutMs }) {
861
1178
  }
862
1179
  if (!installResult.ok) return installResult;
863
1180
 
864
- return runPostInstallCleanup(home, runtime, dryRun, stdio, timeoutMs);
1181
+ const cleanupResult = await runPostInstallCleanup(home, runtime, dryRun, stdio, timeoutMs);
1182
+ if (cleanupResult.ok && !dryRun) {
1183
+ warnDisabledMCP(home);
1184
+ // The guard and policy protect memory, so the companion check only warns.
1185
+ const companion = bundledInstall ? await ensureEngramCompanion(home, runtime, stdio, timeoutMs) : { ok: true };
1186
+ if (!companion.ok) process.stderr.write(`${companion.message}\n`);
1187
+ }
1188
+ return cleanupResult;
1189
+ }
1190
+
1191
+ function mcpAdapterActivity(packages) {
1192
+ if (typeof packages === "string") return packages === "npm:pi-mcp-adapter" || packages.startsWith("npm:pi-mcp-adapter@") ? "active" : "inactive";
1193
+ if (Array.isArray(packages)) {
1194
+ const activities = packages.map(mcpAdapterActivity);
1195
+ return activities.includes("active") ? "active" : activities.includes("unknown") ? "unknown" : "inactive";
1196
+ }
1197
+ if (packages === null || typeof packages !== "object") return "inactive";
1198
+ if (typeof packages.source !== "string") return mcpAdapterActivity(Object.keys(packages));
1199
+ if (mcpAdapterActivity(packages.source) === "inactive") return "inactive";
1200
+ const patterns = packages.extensions;
1201
+ if (patterns === undefined) return packages.autoload === false ? "inactive" : "active";
1202
+ if (!Array.isArray(patterns)) return "unknown";
1203
+ if (patterns.length === 0) return "inactive";
1204
+ if (packages.autoload === false) {
1205
+ return patterns.some(pattern => typeof pattern === "string" && !pattern.startsWith("!") && !pattern.startsWith("-")) ? "unknown" : "inactive";
1206
+ }
1207
+ // Ordinary exclusions run after includes, but exact +path entries can
1208
+ // restore resources. Other filters need actual package files to resolve.
1209
+ if (patterns.includes("!**") && !patterns.some(pattern => typeof pattern === "string" && pattern.startsWith("+"))) return "inactive";
1210
+ return "unknown";
1211
+ }
1212
+
1213
+ // Onboarding cannot prove who disabled MCP: diagnose, never re-enable servers.
1214
+ // This also covers automatic provisioning because both routes share setup.
1215
+ function warnDisabledMCP(home) {
1216
+ const settingsPath = join(home.dir, "settings.json");
1217
+ const mcpPath = join(home.dir, "mcp.json");
1218
+ let inspecting = settingsPath;
1219
+ try {
1220
+ const settingsText = readJsonIfExists(settingsPath);
1221
+ if (settingsText === undefined) return;
1222
+ const settings = JSON.parse(settingsText);
1223
+ if (!Array.isArray(settings?.extensions) || !settings.extensions.includes("-builtin:mcp")) return;
1224
+ const activity = mcpAdapterActivity(settings.packages);
1225
+ if (activity === "active") return;
1226
+ inspecting = mcpPath;
1227
+ const mcpText = readJsonIfExists(mcpPath);
1228
+ if (mcpText === undefined) return;
1229
+ const servers = JSON.parse(mcpText)?.mcpServers;
1230
+ if (servers === null || typeof servers !== "object" || Array.isArray(servers) || Object.keys(servers).length === 0) return;
1231
+ const diagnosis = activity === "unknown"
1232
+ ? `pi-mcp-adapter activity cannot be confirmed from its extension filters, so servers in ${mcpPath} may not load`
1233
+ : `pi-mcp-adapter is absent or inactive, so servers in ${mcpPath} will not load`;
1234
+ process.stderr.write(`gentle-shell: WARNING: Pi built-in MCP is disabled in ${settingsPath}; ${diagnosis}. If you want these servers enabled, remove -builtin:mcp from extensions in ${settingsPath}, then restart Pi. If MCP is intentionally disabled, keep the setting.\n`);
1235
+ } catch (error) {
1236
+ process.stderr.write(`gentle-shell: WARNING: Pi MCP configuration could not be inspected in ${inspecting}: ${error.message}; inspect or repair it manually.\n`);
1237
+ }
865
1238
  }
866
1239
 
867
1240
  // The stderr line printed once `source` is actually removed from `home`.
@@ -941,7 +1314,11 @@ async function removePostInstallSources(sources, index, home, runtime, stdio, ti
941
1314
  // --dry-run, runs the shared flow with the child's stdio inherited (today's
942
1315
  // behavior, unchanged), then exits with its result — this is the one place
943
1316
  // that keeps the pre-S7 exit semantics `handleSetupCommand` always had.
944
- async function handleSetupCommand(commandArgs, home, runtime) {
1317
+ // A successful real run records the home exactly as auto-provisioning does
1318
+ // (#2106: both installers finish with this command, and the first launch
1319
+ // must not install the packages again), but only for a home auto-provisioning
1320
+ // itself would own: never pi's own default home or an unrelated directory.
1321
+ async function handleSetupCommand(commandArgs, home, runtime, { homeHadContentBeforeBootstrap }) {
945
1322
  let dryRun = false;
946
1323
  for (const arg of commandArgs) {
947
1324
  if (arg === "--dry-run") {
@@ -953,6 +1330,19 @@ async function handleSetupCommand(commandArgs, home, runtime) {
953
1330
 
954
1331
  const result = await runSetupFlow(home, runtime, { dryRun, stdio: "inherit" });
955
1332
  if (!result.ok && result.message !== undefined) process.stderr.write(`${result.message}\n`);
1333
+ if (result.ok && !dryRun && (home.mode === "isolated" || home.mode === "path")) {
1334
+ const configPath = resolveConfigPath();
1335
+ const homeKey = safeRealpath(home.dir);
1336
+ const config = readRawConfig(configPath);
1337
+ if (!homeIsForeign(home, provisionedEntry(config, homeKey), homeHadContentBeforeBootstrap)) {
1338
+ try {
1339
+ writeRawConfig(configPath, recordProvisioned(config, homeKey, resolveSetupGentleAiPin(), ownPackageVersion(), new Date().toISOString()));
1340
+ } catch (error) {
1341
+ // Setup itself succeeded; the next launch only sets the home up again.
1342
+ process.stderr.write(`gentle-shell: could not record the setup in ${configPath} (${error.message})\n`);
1343
+ }
1344
+ }
1345
+ }
956
1346
  process.exit(result.exitCode);
957
1347
  }
958
1348
 
@@ -1179,29 +1569,269 @@ async function maybeAutoProvisionHome(home, runtime, { homeHadContentBeforeBoots
1179
1569
  }
1180
1570
  }
1181
1571
 
1572
+ // The adapters `upgrade` and `self-uninstall` run npm and pnpm with: { fs, which, run, invocation? }.
1573
+ async function packageManagerAdapters() {
1574
+ const { hostAdapters } = await import("../scripts/installer-probes.mjs");
1575
+ const { upgradeInvocation } = await import("../scripts/installer-runner.mjs");
1576
+ const { run, fs: probeFs } = hostAdapters();
1577
+ // Windows: npm and pnpm are .cmd shims; run what they run, never through cmd.exe.
1578
+ const invocation = upgradeInvocation({ platform: process.platform, env: process.env, run, fs: probeFs });
1579
+ return {
1580
+ fs: await import("node:fs/promises"),
1581
+ which: async (name) => findOnPath(name) ?? null,
1582
+ ...(invocation ? { invocation } : {}),
1583
+ run: (command, argv, options = {}) => run(command, argv, { env: options.env ?? process.env, cwd: options.cwd, deadlineMs: options.deadlineMs ?? 20 * 60_000 }),
1584
+ };
1585
+ }
1586
+
1587
+ // `gentle-shell upgrade`: runs before any Pi runtime check, since it may replace this package.
1588
+ // On release, a global pnpm or npm install moves to the bundled one when the
1589
+ // latest release publishes its assets (S4), with one question about the old package.
1590
+ async function handleUpgradeCommand(commandArgs) {
1591
+ const { MainChannelError, runUpgrade } = await import("../scripts/main-channel.mjs");
1592
+ const { fetchDistribution, migrateGlobalShell, windowsPathRegistry } = await import("../scripts/bundled-wizard.mjs");
1593
+ const { askLine } = await import("../runtime/gentle-shell-uninstall.mjs");
1594
+ const managers = await packageManagerAdapters();
1595
+ const out = (line) => process.stdout.write(`${line}\n`);
1596
+ const migrate = ({ latest, owner }) => migrateGlobalShell({
1597
+ latest,
1598
+ currentVersion: ownPackageVersion(),
1599
+ platform: process.platform,
1600
+ arch: process.arch,
1601
+ env: process.env,
1602
+ home: homedir(),
1603
+ interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
1604
+ // End of input or Ctrl+C answers No.
1605
+ ask: (question) => askLine(question, { input: process.stdin, output: process.stdout }).catch(() => ""),
1606
+ out,
1607
+ adapters: { owner, distribution: (version) => fetchDistribution({ version }), run: managers.run,
1608
+ ...(process.platform === "win32" ? { registry: windowsPathRegistry(process.env) } : {}) },
1609
+ });
1610
+ try {
1611
+ process.exitCode = await runUpgrade({
1612
+ args: commandArgs,
1613
+ ctx: { env: process.env, home: homedir() },
1614
+ platform: process.platform,
1615
+ packageRoot,
1616
+ currentVersion: ownPackageVersion(),
1617
+ adapters: { fetch: globalThis.fetch, ...managers, migrate },
1618
+ out,
1619
+ });
1620
+ } catch (error) {
1621
+ if (error instanceof MainChannelError) fail(`gentle-shell upgrade: ${error.message}`, error.code === "upgrade-usage" ? 2 : 1);
1622
+ throw error;
1623
+ }
1624
+ }
1625
+
1626
+ // `gentle-shell upgrade` in a bundled install: the latest release installed
1627
+ // beside the active version, verified, then `current` switched (two versions
1628
+ // kept), or `--rollback` to the kept one. Returns the exit code (a failure is
1629
+ // printed, not thrown, so `update --all` can go on); a failure leaves `current`
1630
+ // as it was.
1631
+ async function handleBundledUpgrade(bundled, commandArgs) {
1632
+ const parsed = parseBundledUpgradeArgs(commandArgs);
1633
+ if ("error" in parsed) fail(`gentle-shell upgrade: ${parsed.error}`, parsed.usage ? 2 : 1);
1634
+ const { prefixLayout, rollbackBundled, upgradeBundled } = await import("../scripts/bundled-install.mjs");
1635
+ const layout = prefixLayout({ platform: process.platform, env: process.env, home: homedir(), root: bundled.root });
1636
+ const out = (line) => process.stdout.write(`${line}\n`);
1637
+ try {
1638
+ if (parsed.action === "rollback") {
1639
+ const { from, to } = await rollbackBundled({ layout, home: homedir(), platform: process.platform, arch: process.arch, env: process.env });
1640
+ out(`Switched Gentle Shell from ${from} to ${to}.`);
1641
+ return 0;
1642
+ }
1643
+ const { fetchDistribution } = await import("../scripts/bundled-wizard.mjs");
1644
+ const { latestRelease } = await import("../scripts/main-channel.mjs");
1645
+ await upgradeBundled({
1646
+ layout,
1647
+ home: homedir(),
1648
+ platform: process.platform,
1649
+ arch: process.arch,
1650
+ env: process.env,
1651
+ adapters: { latest: () => latestRelease(globalThis.fetch), distribution: (version) => fetchDistribution({ version }) },
1652
+ out,
1653
+ });
1654
+ return 0;
1655
+ } catch (error) {
1656
+ process.stderr.write(`gentle-shell upgrade: ${error.message}${error.stderr ? `\n${error.stderr}` : ""}\n`);
1657
+ return 1;
1658
+ }
1659
+ }
1660
+
1661
+ // The bundled npm as the `npmCommand` of a home Gentle Shell owns (the isolated
1662
+ // home, or a --home it created or provisioned; never --link or a foreign home),
1663
+ // so Pi installs packages with our npm while its tools keep the user's PATH.
1664
+ // Only that key is written, and only when it differs.
1665
+ async function ensureBundledNpmCommand(bundled, home, homeHadContentBeforeBootstrap) {
1666
+ if (home.mode === "link") return;
1667
+ const previous = provisionedEntry(readRawConfig(resolveConfigPath()), safeRealpath(home.dir));
1668
+ if (homeIsForeign(home, previous, homeHadContentBeforeBootstrap) || !isDirectory(home.dir)) return;
1669
+ const { bundledNpmCommand, ensureNpmShim, prefixLayout } = await import("../scripts/bundled-install.mjs");
1670
+ const layout = prefixLayout({ platform: process.platform, env: process.env, home: homedir(), root: bundled.root });
1671
+ // The entry npmCommand names, also for a runtime installed before it existed.
1672
+ ensureNpmShim(layout);
1673
+ const settingsPath = join(home.dir, "settings.json");
1674
+ const edit = withBundledNpmCommand(readJsonIfExists(settingsPath), bundledNpmCommand(layout));
1675
+ if (edit.status === "malformed") {
1676
+ process.stderr.write(`gentle-shell: ${settingsPath} is not a JSON object, so Pi's package installs use the npm on your PATH\n`);
1677
+ } else if (edit.status === "write") {
1678
+ writeFileAtomically(settingsPath, edit.text);
1679
+ }
1680
+ }
1681
+
1682
+ // `gentle-shell self-uninstall` in a bundled install (S6, S7): the prefix's own
1683
+ // folders and exactly the PATH entry its receipt records; on Windows the running
1684
+ // node.exe and launcher are removed by a detached process after this one exits.
1685
+ async function handleBundledSelfUninstall(bundled, commandArgs) {
1686
+ const { askLine, runBundledSelfUninstall } = await import("../runtime/gentle-shell-uninstall.mjs");
1687
+ const { describePathReceipt, prefixLayout, readPathReceipt, removePathEntry, scheduleWindowsRemoval } = await import("../scripts/bundled-install.mjs");
1688
+ const windows = process.platform === "win32";
1689
+ const layout = prefixLayout({ platform: process.platform, env: process.env, home: homedir(), root: bundled.root });
1690
+ // Trusted only for the link or startup file the PATH plan can choose for this user.
1691
+ const receipt = readPathReceipt(layout, { env: process.env, home: homedir() });
1692
+ let pathEntry = receipt;
1693
+ if (receipt.status === "recorded") {
1694
+ const registry = windows ? (await import("../scripts/bundled-wizard.mjs")).windowsPathRegistry(process.env) : undefined;
1695
+ pathEntry = { status: "recorded", description: describePathReceipt(receipt.receipt), remove: () => removePathEntry(receipt.receipt, { registry }) };
1696
+ }
1697
+ return runBundledSelfUninstall({
1698
+ args: commandArgs,
1699
+ env: process.env,
1700
+ homedir: homedir(),
1701
+ platform: process.platform,
1702
+ interactive: process.stdin.isTTY === true,
1703
+ root: bundled.root,
1704
+ pathEntry,
1705
+ process: { pid: process.pid, execPath: process.execPath },
1706
+ ...(windows ? { deferRemoval: (request) => scheduleWindowsRemoval({ ...request, env: process.env }) } : {}),
1707
+ ask: (question) => askLine(question, { input: process.stdin, output: process.stdout }),
1708
+ out: (line) => process.stdout.write(`${line}\n`),
1709
+ err: (line) => process.stderr.write(`${line}\n`),
1710
+ });
1711
+ }
1712
+
1713
+ // `gentle-shell self-uninstall`: like upgrade, runs before any Pi runtime check,
1714
+ // so it also works when pi is missing.
1715
+ async function handleSelfUninstallCommand(commandArgs) {
1716
+ const { askLine, runSelfUninstall } = await import("../runtime/gentle-shell-uninstall.mjs");
1717
+ const { ownerManager } = await import("../scripts/main-channel.mjs");
1718
+ const managers = await packageManagerAdapters();
1719
+ const ctx = { env: process.env, home: homedir() };
1720
+ process.exitCode = await runSelfUninstall({
1721
+ args: commandArgs,
1722
+ env: process.env,
1723
+ homedir: homedir(),
1724
+ platform: process.platform,
1725
+ interactive: process.stdin.isTTY === true,
1726
+ resolveOwner: () => ownerManager({ ctx, platform: process.platform, packageRoot, adapters: managers, command: "uninstall" }),
1727
+ run: (command, argv) => managers.run(command, argv),
1728
+ ask: (question) => askLine(question, { input: process.stdin, output: process.stdout }),
1729
+ out: (line) => process.stdout.write(`${line}\n`),
1730
+ err: (line) => process.stderr.write(`${line}\n`),
1731
+ });
1732
+ }
1733
+
1734
+ // Before Pi exists: only the bundled interactive opening may offer an update.
1735
+ async function startupUpdate(bundled, args, startupEnv) {
1736
+ if (!shouldCheckStartupUpdate({ args, env: startupEnv, bundled: Boolean(bundled),
1737
+ inputTTY: process.stdin.isTTY === true, outputTTY: process.stdout.isTTY === true })) return undefined;
1738
+ const configPath = resolveConfigPath();
1739
+ let skipped;
1740
+ try { skipped = readRawConfig(configPath).skippedStartupRelease; } catch { /* A notice must not prevent opening. */ }
1741
+ const { latestRelease } = await import("../scripts/main-channel.mjs");
1742
+ const offer = await findStartupUpdate({ current: ownPackageVersion(), skipped,
1743
+ lookup: (signal) => latestRelease((url, options) => fetch(url, { ...options, signal })) });
1744
+ if (!offer) return undefined;
1745
+ let choice;
1746
+ try { choice = await promptStartupUpdate(offer, { color: process.stdout.hasColors?.() === true }); }
1747
+ catch { return undefined; }
1748
+ return applyStartupChoice(choice, {
1749
+ omit: () => omitStartupVersion(() => readJsonIfExists(configPath), (text) => {
1750
+ mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
1751
+ writeFileAtomically(configPath, text, { mode: 0o600 });
1752
+ }, offer.latest),
1753
+ warn: (message) => process.stderr.write(`gentle-shell: ${message}\n`),
1754
+ upgrade: () => handleBundledUpgrade(bundled, []),
1755
+ relaunch: async () => {
1756
+ const next = await resolveStartupRelaunch({ root: bundled.root, home: homedir(), platform: process.platform,
1757
+ env: process.env, argv: process.argv.slice(2) });
1758
+ return new Promise((resolve, reject) => {
1759
+ const child = spawn(next.command, next.args, { stdio: "inherit", env: next.env, shell: false });
1760
+ const forwards = ["SIGINT", "SIGTERM", "SIGHUP"].map((signal) => {
1761
+ const handler = () => child.kill(signal);
1762
+ process.on(signal, handler);
1763
+ return [signal, handler];
1764
+ });
1765
+ const cleanup = () => { for (const [signal, handler] of forwards) process.off(signal, handler); };
1766
+ child.once("error", (error) => { cleanup(); reject(error); });
1767
+ child.once("exit", (code, signal) => { cleanup(); resolve(signal ? signalExitCode(signal) : (code ?? 1)); });
1768
+ });
1769
+ },
1770
+ });
1771
+ }
1772
+
1182
1773
  async function main() {
1774
+ // One launch only: never leak the update handoff opt-out into Pi or its tools.
1775
+ const startupEnv = { ...process.env };
1776
+ delete process.env.GENTLE_SHELL_SKIP_STARTUP_UPDATE;
1183
1777
  const args = parseLauncherArgs(process.argv.slice(2));
1184
1778
  if (args.error !== undefined) fail(`${args.error}\nRun 'gentle-shell --help' for usage.`, 2);
1185
1779
  if (args.help) {
1186
- process.stdout.write(`${helpText()}\n`);
1780
+ process.stdout.write("Pi updates: update reinstalls the verified supplier of the Pi this launcher runs (a global Pi, or the\nShell package whose adjacent peer it is, at the same Shell version) and reads the resulting Pi version back.\nUnknown ownership refuses without installing; update --extensions-only stays Pi-only.\nPrefix-bundled Pi updates with gentle-shell upgrade.\n");
1781
+ process.stdout.write(`${helpText()}\nBundled Engram safety: memory opens only a fresh, verified or Shell-provisioned store; otherwise ENGRAM_BIN\nis Shell's guard and memory stays off. Matching versions or updating alone do not prove compatibility.\nReuse requires a server with verified /compatibility evidence or matching Shell-provisioned store identity.\nBack up existing data before manual changes; do not delete it or bypass the safety check.\nShell rechecks on every launch and remains usable without memory.\nAn explicit ENGRAM_BIN or ENGRAM_URL is kept and bypasses the guard.\n`);
1187
1782
  process.exit(0);
1188
1783
  }
1189
1784
  if (args.command === "home") {
1190
1785
  handleHomeCommand(args.commandArgs);
1191
1786
  return;
1192
1787
  }
1788
+ // The verification input applies to `--version` only (scripts/bundled-install.mjs upgradeBundled).
1789
+ const verifying = args.version && args.command === undefined ? process.env[BUNDLED_VERIFY_ENV] : undefined;
1790
+ const bundled = resolveBundledInstall({ packageRoot, platform: process.platform, fs: bundledFs, verifying });
1791
+ if (args.command === "upgrade") {
1792
+ if (bundled) process.exitCode = await handleBundledUpgrade(bundled, args.commandArgs);
1793
+ else await handleUpgradeCommand(args.commandArgs);
1794
+ return;
1795
+ }
1796
+ if (args.command === "self-uninstall") {
1797
+ if (bundled) process.exitCode = await handleBundledSelfUninstall(bundled, args.commandArgs);
1798
+ else await handleSelfUninstallCommand(args.commandArgs);
1799
+ return;
1800
+ }
1801
+
1802
+ const startupCode = await startupUpdate(bundled, args, startupEnv);
1803
+ if (startupCode !== undefined) {
1804
+ process.exitCode = startupCode;
1805
+ return;
1806
+ }
1193
1807
 
1194
1808
  const config = loadConfig();
1195
1809
  let home = resolveHome({ args, env: process.env, homedir: homedir(), config });
1196
1810
  if (home.mode === "path") home = { ...home, dir: resolvePath(home.dir) };
1197
1811
 
1198
- const runtime = resolvePiRuntime({
1199
- env: process.env,
1200
- resolveBundledCli,
1201
- findOnPath,
1202
- nodeExecPath: process.execPath,
1203
- });
1204
- if (runtime === undefined) fail(missingPiMessage(), 1);
1812
+ // Version and help runs (Shell's own or forwarded to Pi) never probe Engram,
1813
+ // ensure the companion, print memory notices or touch provenance.
1814
+ const engramQuiet = args.version || piVersionOrHelpRequested(args.passthrough);
1815
+ let runtime;
1816
+ if (bundled) {
1817
+ // S8: only the version's own Pi, never GENTLE_SHELL_PI, an adjacent fallback or PATH.
1818
+ const pi = bundledPi(bundled, { platform: process.platform, fs: bundledFs });
1819
+ if (!pi.ok) fail(pi.message, 1);
1820
+ runtime = { kind: "bundled", command: process.execPath, args: [pi.cli] };
1821
+ bundledInstall = bundled;
1822
+ bundledCli = pi.cli;
1823
+ bundledMemory = engramQuiet ? { policy: "disabled", notice: ENGRAM_RECOVERY_NOTICE } : await classifyBundledMemory();
1824
+ bundledEnv = bundledProcessEnv();
1825
+ bundledSetupDirs = [dirname(process.execPath), join(bundled.versionDir, "node_modules", ".bin")];
1826
+ } else {
1827
+ runtime = resolvePiRuntime({
1828
+ env: process.env,
1829
+ resolveBundledCli,
1830
+ findOnPath,
1831
+ nodeExecPath: process.execPath,
1832
+ });
1833
+ if (runtime === undefined) fail(missingPiMessage(), 1);
1834
+ }
1205
1835
 
1206
1836
  const versionProbePlan = planSpawn({ command: runtime.command, args: [...runtime.args, "--version"], platform: process.platform });
1207
1837
  const versionProbe = spawnSync(versionProbePlan.command, versionProbePlan.args, {
@@ -1209,6 +1839,7 @@ async function main() {
1209
1839
  timeout: 15000,
1210
1840
  encoding: "utf8",
1211
1841
  shell: versionProbePlan.shell,
1842
+ env: bundledEnv ?? process.env,
1212
1843
  });
1213
1844
  if (versionProbe.error) fail(`Could not run the pi runtime at "${runtime.command}": ${versionProbe.error.message}`, 1);
1214
1845
  const versionCheck = checkPiVersion(versionProbe.stdout ?? "");
@@ -1219,6 +1850,32 @@ async function main() {
1219
1850
  process.exit(0);
1220
1851
  }
1221
1852
 
1853
+ // S5: in a bundled install Pi updates only with Gentle Shell, so Pi's own
1854
+ // update becomes `gentle-shell upgrade` (run when interactive, named
1855
+ // otherwise); package updates still reach Pi.
1856
+ let passthrough = args.passthrough;
1857
+ if (bundled && args.piSubcommand === "update") {
1858
+ const plan = bundledUpdatePlan(args.passthrough);
1859
+ if (plan.self) {
1860
+ const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
1861
+ for (const line of bundledUpdateNotice({ piVersion: versionCheck.version, interactive })) process.stdout.write(`${line}\n`);
1862
+ const code = interactive ? await handleBundledUpgrade(bundled, []) : undefined;
1863
+ const next = bundledUpdateNext(plan, code);
1864
+ if ("exitCode" in next) process.exit(next.exitCode);
1865
+ if (next.note) process.stderr.write(`${next.note}\n`);
1866
+ }
1867
+ passthrough = plan.piArgs;
1868
+ } else if (args.piSubcommand === "update") {
1869
+ const { managedUpdatePlan, updateSelectedPi } = await import("../scripts/pi-update.mjs");
1870
+ const plan = managedUpdatePlan(args.passthrough);
1871
+ if (plan.self) {
1872
+ process.exitCode = await updateSelectedPi({ runtime, packageRoot, platform: process.platform, currentVersion: versionCheck.version, cwd: tmpdir(),
1873
+ adapters: await packageManagerAdapters(), out: (line) => process.stdout.write(`${line}\n`) });
1874
+ if (plan.piArgs === undefined) return;
1875
+ }
1876
+ passthrough = plan.piArgs;
1877
+ }
1878
+
1222
1879
  // Home-ownership signal for auto-provisioning (S9, homeIsForeign): must be
1223
1880
  // read before the isolated-home bootstrap below creates and seeds a
1224
1881
  // brand-new --home directory with its own settings.json — after that
@@ -1236,14 +1893,24 @@ async function main() {
1236
1893
  // Isolated-home bootstrap: only on a home gentle-shell has not seen before
1237
1894
  // (link never bootstraps — it reuses the user's own pi agent home as-is).
1238
1895
  if ((home.mode === "isolated" || home.mode === "path") && !existsSync(home.dir)) {
1239
- mkdirSync(home.dir, { recursive: true });
1896
+ // 0700: under umask 002 a default mode would leave ~/.gentle-shell group-writable,
1897
+ // which the bundled install's prefix claim must tighten or refuse.
1898
+ mkdirSync(home.dir, { recursive: true, mode: 0o700 });
1240
1899
  await installIsolatedTuiModeSetting(home.dir);
1241
1900
  writeHomeOwnershipMarker(home); // R3-001: lets a failed first auto-provision attempt still be retried later
1242
1901
  process.stderr.write(`gentle-shell: using a separate home at ${home.dir}. Run 'gentle-shell --link' to reuse your pi sign-ins and chats.\n`);
1243
1902
  }
1244
1903
 
1904
+ if (bundled) {
1905
+ try {
1906
+ await ensureBundledNpmCommand(bundled, home, homeHadContentBeforeBootstrap);
1907
+ } catch (error) {
1908
+ process.stderr.write(`gentle-shell: could not give Pi the bundled npm in ${join(home.dir, "settings.json")} (${error.message}); continuing\n`);
1909
+ }
1910
+ }
1911
+
1245
1912
  if (args.command === "setup") {
1246
- await handleSetupCommand(args.commandArgs, home, runtime);
1913
+ await handleSetupCommand(args.commandArgs, home, runtime, { homeHadContentBeforeBootstrap });
1247
1914
  return;
1248
1915
  }
1249
1916
 
@@ -1371,6 +2038,23 @@ async function main() {
1371
2038
  }
1372
2039
  }
1373
2040
 
2041
+ if (bundled) {
2042
+ // Recheck after setup and before provider loading: never carry a stale
2043
+ // fresh-store decision across an installer that could create data.
2044
+ if (args.piSubcommand === undefined && !engramQuiet) {
2045
+ bundledMemory = await classifyBundledMemory();
2046
+ bundledEnv = bundledProcessEnv();
2047
+ // The guard protects memory whatever provider loads, so a companion that
2048
+ // could not be verified is reported, never a reason to stop Shell.
2049
+ const companion = await ensureEngramCompanion(home, runtime, ["ignore", 2, 2], 10000);
2050
+ if (!companion.ok) process.stderr.write(`${companion.message}\n`);
2051
+ // A fresh store's identity and provenance exist before Pi starts, or
2052
+ // memory stays off: Pi never runs with allow without a written record.
2053
+ if (bundledMemory.fresh && !provisionEngramStore()) bundledMemory = { policy: "disabled", notice: ENGRAM_PROVISION_FAILED_NOTICE };
2054
+ if (bundledMemory.notice) process.stderr.write(`gentle-shell: ${bundledMemory.notice}\n`);
2055
+ }
2056
+ bundledEnv = bundledProcessEnv();
2057
+ }
1374
2058
  const invocation = buildPiInvocation({
1375
2059
  runtime,
1376
2060
  home,
@@ -1379,10 +2063,12 @@ async function main() {
1379
2063
  takeOver,
1380
2064
  otherPackagePaths,
1381
2065
  looseExtensionEntries,
1382
- passthrough: [...managedHerdrExtensionArgs(home, args), ...args.passthrough],
2066
+ passthrough: [...managedHerdrExtensionArgs(home, args), ...passthrough],
1383
2067
  piSubcommand: args.piSubcommand,
1384
- baseEnv: process.env,
2068
+ baseEnv: bundledEnv ?? process.env,
1385
2069
  homedir: homedir(),
2070
+ // The spawn below sets no cwd, so pi runs in the launcher's own.
2071
+ cwd: process.cwd(),
1386
2072
  });
1387
2073
 
1388
2074
  // Only an interactive session ends with pi's exit resume hint, which