gentle-pi 3.7.0 → 4.1.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 (590) hide show
  1. package/README.md +155 -10
  2. package/assets/agents/gentle-ai-explore.md +13 -6
  3. package/assets/agents/gentle-ai-verify.md +27 -6
  4. package/assets/agents/gentle-ai-worker.md +13 -15
  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 +75 -169
  9. package/assets/orchestrator-memory.md +12 -26
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-skills.md +1 -1
  12. package/assets/orchestrator-tracking.md +53 -0
  13. package/assets/orchestrator-verification.md +28 -0
  14. package/assets/orchestrator-writer.md +38 -0
  15. package/assets/orchestrator.md +30 -38
  16. package/assets/sounds/LICENSE.md +52 -0
  17. package/assets/sounds/attention.wav +0 -0
  18. package/assets/sounds/error.wav +0 -0
  19. package/assets/sounds/success.wav +0 -0
  20. package/assets/support/strict-tdd-verify.md +4 -266
  21. package/assets/support/strict-tdd.md +8 -360
  22. package/bin/engram-guard +9 -0
  23. package/bin/gentle-shell-install.mjs +302 -0
  24. package/bin/gentle-shell.mjs +956 -50
  25. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  27. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  28. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  29. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  30. package/docs/bundled-install.md +397 -0
  31. package/docs/delegated-verification.md +27 -4
  32. package/docs/gentle-agents-activity.md +537 -0
  33. package/docs/gentle-shell-uninstall.md +136 -0
  34. package/docs/gentle-shell.md +165 -51
  35. package/docs/install-wizard.md +2040 -0
  36. package/docs/native-authority-architecture.md +3 -3
  37. package/docs/permission-modes.md +82 -0
  38. package/docs/prompt-history.md +280 -0
  39. package/docs/readme-reference.md +364 -231
  40. package/docs/render-cache-performance.md +28 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/telemetry.md +1 -1
  44. package/docs/yolo-mode.md +86 -0
  45. package/extensions/child-context.ts +35 -0
  46. package/extensions/child-safety.ts +25 -0
  47. package/extensions/codegraph-tools.ts +18 -5
  48. package/extensions/gentle-agents.ts +1093 -326
  49. package/extensions/gentle-ai.ts +1636 -890
  50. package/extensions/gentle-herdr-activity.ts +81 -0
  51. package/extensions/gentle-notifications.ts +104 -0
  52. package/extensions/gentle-shell.ts +1619 -136
  53. package/extensions/gentle-stats.ts +101 -0
  54. package/extensions/gentle-todo.ts +126 -20
  55. package/extensions/history/atomic-write.ts +38 -0
  56. package/extensions/history/hide-prompts.ts +183 -0
  57. package/extensions/history/index.ts +1424 -0
  58. package/extensions/history/load-shared-history.ts +39 -0
  59. package/extensions/history/selector-helpers.ts +538 -0
  60. package/extensions/history/session-scan.ts +233 -0
  61. package/extensions/history/store.ts +1119 -0
  62. package/extensions/nan-provider.ts +6 -0
  63. package/extensions/pi-pretty.ts +39 -3
  64. package/extensions/quiet-tools.ts +254 -92
  65. package/extensions/resume-hint.ts +60 -0
  66. package/extensions/skill-registry.ts +191 -58
  67. package/extensions/startup-banner.ts +71 -33
  68. package/lib/agent-assets.ts +604 -0
  69. package/lib/agent-home.ts +10 -1
  70. package/lib/agent-model-resolution.ts +80 -0
  71. package/lib/agent-profile-pin.ts +12 -0
  72. package/lib/agent-profiles.ts +13 -3
  73. package/lib/agents-completion-delivery.ts +11 -3
  74. package/lib/agents-message-delivery.ts +181 -0
  75. package/lib/agents-messaging.ts +1 -1
  76. package/lib/agents-protocol.ts +99 -0
  77. package/lib/agents-runner.ts +180 -107
  78. package/lib/agents-view.ts +26 -3
  79. package/lib/agents-widget.ts +16 -9
  80. package/lib/append-system-prompt.ts +21 -0
  81. package/lib/auto-permission-classifier.ts +280 -0
  82. package/lib/auto-permission-runtime.ts +268 -0
  83. package/lib/background-jobs-tools.ts +202 -0
  84. package/lib/background-jobs.ts +253 -0
  85. package/lib/background-monitor.ts +154 -0
  86. package/lib/bounded-writer-admission.ts +220 -0
  87. package/lib/builtin-codemode-optout.ts +166 -0
  88. package/lib/card-content-policy.ts +85 -0
  89. package/lib/card-style-policy.ts +60 -0
  90. package/lib/child-context-files.ts +166 -0
  91. package/lib/child-package-injection.ts +62 -0
  92. package/lib/codemode-renderer.ts +195 -0
  93. package/lib/command-palette-catalog.ts +5 -10
  94. package/lib/command-palette.ts +25 -14
  95. package/lib/context-threshold.ts +26 -0
  96. package/lib/destructive-command-guard.ts +144 -0
  97. package/lib/gentle-ai-binary.ts +109 -4
  98. package/lib/gentle-ai-elapsed-store.ts +87 -0
  99. package/lib/gentle-ai-renderer.ts +196 -39
  100. package/lib/gentle-shell-bundled.ts +517 -0
  101. package/lib/gentle-shell-launcher.ts +89 -27
  102. package/lib/gentle-shell-resume-hint.ts +176 -0
  103. package/lib/gentle-shell-startup-flow.ts +54 -0
  104. package/lib/gentle-shell-startup-update.ts +92 -0
  105. package/lib/gentle-shell-uninstall.ts +840 -0
  106. package/lib/gentle-shell-update-panel.ts +231 -0
  107. package/lib/herdr-activity.ts +183 -0
  108. package/lib/history-capture-policy.ts +95 -0
  109. package/lib/inprocess-reviewer.ts +3 -0
  110. package/lib/jobs-sidebar-state.ts +20 -0
  111. package/lib/jobs-view.ts +161 -0
  112. package/lib/model-price-ratio.ts +83 -0
  113. package/lib/model-routing-authority.ts +5 -1
  114. package/lib/nan-provider.ts +249 -0
  115. package/lib/native-review-cli.ts +242 -180
  116. package/lib/notification-audio-native.ts +162 -0
  117. package/lib/notification-audio-windows.ts +399 -0
  118. package/lib/notification-audio.ts +332 -0
  119. package/lib/notification-customize.ts +241 -0
  120. package/lib/notification-events.ts +113 -0
  121. package/lib/notification-policy.ts +148 -0
  122. package/lib/notification-pulse-client.ts +367 -0
  123. package/lib/notification-pulse-protocol.ts +305 -0
  124. package/lib/notification-pulse-stream.ts +213 -0
  125. package/lib/notification-pulse-worker.ts +66 -0
  126. package/lib/notification-scheduler.ts +173 -0
  127. package/lib/notification-service.ts +202 -0
  128. package/lib/notification-ui.ts +87 -0
  129. package/lib/odd-phase-inference.ts +231 -0
  130. package/lib/odd-phase.ts +141 -0
  131. package/lib/orchestrator-alias.ts +20 -0
  132. package/lib/orchestrator-catalog.ts +79 -0
  133. package/lib/orchestrator-consultation.ts +61 -0
  134. package/lib/orchestrator-discovery.ts +49 -0
  135. package/lib/orchestrator-helper-consent.ts +122 -0
  136. package/lib/orchestrator-helper.ts +167 -0
  137. package/lib/orchestrator-message-card.ts +105 -0
  138. package/lib/orchestrator-presence.ts +115 -14
  139. package/lib/orchestrator-scope.ts +80 -0
  140. package/lib/orchestrator-state.ts +107 -0
  141. package/lib/orchestrator-tool-card.ts +280 -0
  142. package/lib/orchestrator-work-search.ts +218 -0
  143. package/lib/orchestrator-work.ts +79 -0
  144. package/lib/overlay-repaint.ts +26 -0
  145. package/lib/permission-mode-policy.ts +59 -0
  146. package/lib/pi-tui-keys.ts +53 -0
  147. package/lib/review-candidate-view-owner.ts +73 -21
  148. package/lib/review-candidate-view.ts +115 -27
  149. package/lib/review-compact-contract.ts +2 -2
  150. package/lib/review-host-relay.ts +7 -2
  151. package/lib/review-integration-v2.ts +100 -22
  152. package/lib/review-publication-gate.ts +1 -1
  153. package/lib/review-reminder-receipt.ts +48 -8
  154. package/lib/review-risk-assessment.ts +227 -56
  155. package/lib/review-sidebar-state.ts +224 -0
  156. package/lib/review-transaction.ts +72 -3
  157. package/lib/runtime-metrics-children.ts +1 -1
  158. package/lib/runtime-metrics-native.ts +3 -2
  159. package/lib/selection-engine.ts +527 -0
  160. package/lib/session-change-capture.ts +5 -4
  161. package/lib/session-changes.ts +27 -22
  162. package/lib/session-messaging-grants.ts +135 -0
  163. package/lib/session-profile-binding.ts +83 -0
  164. package/lib/session-profile-freeze.ts +266 -0
  165. package/lib/session-worktree-registry.ts +87 -9
  166. package/lib/shell-bar.ts +215 -30
  167. package/lib/shell-card.ts +287 -18
  168. package/lib/shell-changes-view.ts +42 -14
  169. package/lib/shell-changes.ts +86 -71
  170. package/lib/shell-prompt.ts +102 -6
  171. package/lib/shell-sidebar-layout.ts +83 -15
  172. package/lib/shell-sidebar.ts +15 -1
  173. package/lib/shell-todo.ts +84 -31
  174. package/lib/shell-usage-view.ts +9 -4
  175. package/lib/shell-usage.ts +70 -13
  176. package/lib/stats-collector.ts +381 -0
  177. package/lib/stats-view.ts +431 -0
  178. package/lib/status-timing-diagnostics.ts +201 -0
  179. package/lib/telemetry-trigger.ts +3 -1
  180. package/lib/theme-customization.ts +52 -0
  181. package/lib/vim-editor-adapter.ts +383 -0
  182. package/lib/vim-normal-engine.ts +154 -0
  183. package/lib/vim-operator-engine.ts +416 -0
  184. package/lib/vim-policy.ts +49 -0
  185. package/lib/vim-visual-engine.ts +107 -0
  186. package/lib/visual-customization-policy.ts +119 -0
  187. package/lib/visual-customize-view.ts +481 -0
  188. package/lib/visual-profiles.ts +228 -0
  189. package/lib/writer-surfaces.ts +162 -0
  190. package/lib/yolo-session-policy.ts +317 -0
  191. package/package.json +20 -8
  192. package/runtime/child-package-injection.mjs +63 -0
  193. package/runtime/gentle-ai-binary.mjs +109 -4
  194. package/runtime/gentle-shell-bundled.mjs +518 -0
  195. package/runtime/gentle-shell-launcher.mjs +87 -25
  196. package/runtime/gentle-shell-resume-hint.mjs +177 -0
  197. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  198. package/runtime/gentle-shell-startup-update.mjs +93 -0
  199. package/runtime/gentle-shell-uninstall.mjs +841 -0
  200. package/runtime/gentle-shell-update-panel.mjs +232 -0
  201. package/runtime/native-review-cli.mjs +239 -177
  202. package/runtime/review-integration-v2.mjs +100 -22
  203. package/runtime/review-risk-assessment.mjs +225 -54
  204. package/runtime/status-timing-diagnostics.mjs +202 -0
  205. package/runtime/telemetry-trigger.mjs +3 -1
  206. package/scripts/bootstrap.cmd +255 -0
  207. package/scripts/bootstrap.sh +168 -0
  208. package/scripts/build-distribution.mjs +157 -0
  209. package/scripts/build-installer-bundles.mjs +136 -0
  210. package/scripts/build-runtime-modules.mjs +8 -0
  211. package/scripts/bundled-install.mjs +1308 -0
  212. package/scripts/bundled-wizard.mjs +399 -0
  213. package/scripts/check-types.mjs +4 -6
  214. package/scripts/gentle-ai-installer.mjs +83 -35
  215. package/scripts/install-gentle-ai.mjs +48 -1
  216. package/scripts/install-tui-mode-setting.mjs +10 -1
  217. package/scripts/install-wizard-preview.mjs +154 -0
  218. package/scripts/installer-downloads.mjs +648 -0
  219. package/scripts/installer-preflight.mjs +387 -0
  220. package/scripts/installer-probes.mjs +564 -0
  221. package/scripts/installer-runner.mjs +1102 -0
  222. package/scripts/installer-server.mjs +881 -0
  223. package/scripts/installer-windows-artifacts.json +15 -0
  224. package/scripts/installer-windows.mjs +752 -0
  225. package/scripts/main-channel.mjs +452 -0
  226. package/scripts/mirror-odd-routing.mjs +2 -2
  227. package/scripts/npm-pack-result.mjs +31 -0
  228. package/scripts/render-cache-performance-worker.mjs +205 -0
  229. package/scripts/render-cache-performance.mjs +115 -0
  230. package/scripts/run-test-suite.mjs +90 -0
  231. package/scripts/test-installer-acceptance.sh +161 -0
  232. package/scripts/test-packed-runner.mjs +54 -19
  233. package/scripts/types-baseline.json +2 -92
  234. package/scripts/verify-package-files.mjs +91 -38
  235. package/skills/branch-pr/SKILL.md +24 -52
  236. package/skills/chained-pr/SKILL.md +31 -15
  237. package/skills/chained-pr/references/chaining-details.md +31 -20
  238. package/skills/gentle-ai/SKILL.md +16 -21
  239. package/skills/issue-creation/SKILL.md +8 -2
  240. package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
  241. package/skills/work-unit-commits/SKILL.md +4 -3
  242. package/tests/agent-home.test.ts +56 -3
  243. package/tests/agent-model-resolution.test.ts +113 -0
  244. package/tests/agent-profiles.test.ts +30 -0
  245. package/tests/agent-risk-escalation.test.ts +123 -0
  246. package/tests/agents-completion-delivery.test.ts +17 -0
  247. package/tests/agents-fake-child.ts +2 -2
  248. package/tests/agents-message-delivery.test.ts +106 -0
  249. package/tests/agents-protocol.test.ts +97 -0
  250. package/tests/agents-queries.test.ts +9 -9
  251. package/tests/agents-runner-process.test.ts +31 -0
  252. package/tests/agents-runner.test.ts +467 -93
  253. package/tests/agents-view-thread-identity.test.ts +169 -0
  254. package/tests/agents-view.test.ts +8 -2
  255. package/tests/agents-widget.test.ts +154 -15
  256. package/tests/append-system-prompt-route.test.ts +160 -0
  257. package/tests/append-system-prompt.test.ts +46 -0
  258. package/tests/artifact-language.test.ts +19 -213
  259. package/tests/ask-user-question.test.ts +44 -1
  260. package/tests/asset-installation-runtime.test.ts +5 -16
  261. package/tests/auto-mode-invariants.test.ts +258 -0
  262. package/tests/auto-mode-sdk.test.ts +57 -0
  263. package/tests/auto-mode.test.ts +49 -0
  264. package/tests/auto-permission-classifier.test.ts +283 -0
  265. package/tests/auto-permission-runtime.test.ts +196 -0
  266. package/tests/autonomous-guard.test.ts +69 -1
  267. package/tests/background-cache-warming.test.ts +2 -1
  268. package/tests/background-jobs.test.ts +227 -0
  269. package/tests/background-monitor.test.ts +195 -0
  270. package/tests/background-subagents-default-mode.test.ts +28 -5
  271. package/tests/bounded-stop-contract.test.ts +85 -0
  272. package/tests/bounded-writer-admission.test.ts +146 -0
  273. package/tests/branch-pr-skill.test.ts +43 -0
  274. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  275. package/tests/bridge-wake-rendering.test.ts +56 -0
  276. package/tests/build-distribution.test.ts +269 -0
  277. package/tests/builtin-codemode-optout.test.ts +242 -0
  278. package/tests/bundled-install.test.ts +1319 -0
  279. package/tests/bundled-wizard.test.ts +505 -0
  280. package/tests/card-content-policy.test.ts +68 -0
  281. package/tests/card-style-policy.test.ts +87 -0
  282. package/tests/chained-pr-skill.test.ts +124 -0
  283. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  284. package/tests/child-context-files.test.ts +255 -0
  285. package/tests/child-package-entrypoints.test.ts +65 -0
  286. package/tests/child-package-injection.test.ts +74 -0
  287. package/tests/child-safety.test.ts +94 -0
  288. package/tests/child-web-package.test.ts +224 -0
  289. package/tests/codegraph-tools.test.ts +35 -0
  290. package/tests/codemode-rendering.test.ts +596 -0
  291. package/tests/command-palette.test.ts +43 -7
  292. package/tests/context-threshold.test.ts +31 -0
  293. package/tests/delegate-for-reason-contract.test.ts +327 -0
  294. package/tests/delegated-key-learnings-contract.test.ts +3 -78
  295. package/tests/destructive-command-guard.test.ts +84 -0
  296. package/tests/devbinary/native-review-parity.devtest.ts +171 -3
  297. package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
  298. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  299. package/tests/engram-safety.test.ts +288 -0
  300. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
  301. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
  302. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
  303. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
  304. package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
  305. package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
  306. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
  307. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
  308. package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
  309. package/tests/generic-agent-tools.test.ts +69 -0
  310. package/tests/gentle-agents.test.ts +2804 -346
  311. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  312. package/tests/gentle-ai-binary.test.ts +21 -4
  313. package/tests/gentle-ai-child-guards.test.ts +202 -0
  314. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  315. package/tests/gentle-ai-elapsed-store.test.ts +68 -0
  316. package/tests/gentle-ai-installer.test.ts +566 -62
  317. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  318. package/tests/gentle-ai-renderer.test.ts +493 -14
  319. package/tests/gentle-ai.test.ts +1427 -236
  320. package/tests/gentle-card-text.ts +2 -1
  321. package/tests/gentle-herdr-activity.test.ts +100 -0
  322. package/tests/gentle-notifications.test.ts +427 -0
  323. package/tests/gentle-shell-bin.test.ts +896 -115
  324. package/tests/gentle-shell-bundled.test.ts +1002 -0
  325. package/tests/gentle-shell-launcher.test.ts +333 -65
  326. package/tests/gentle-shell-resume-hint.test.ts +270 -0
  327. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  328. package/tests/gentle-shell-startup-update.test.ts +85 -0
  329. package/tests/gentle-shell-uninstall.test.ts +978 -0
  330. package/tests/gentle-shell-update-panel.test.ts +158 -0
  331. package/tests/gentle-shell.test.ts +4216 -119
  332. package/tests/gentle-stats.test.ts +152 -0
  333. package/tests/gentle-theme.test.ts +4 -1
  334. package/tests/gentle-todo.test.ts +333 -10
  335. package/tests/herdr-activity.test.ts +243 -0
  336. package/tests/history-agent-home.test.ts +81 -0
  337. package/tests/history-atomic-write.test.ts +57 -0
  338. package/tests/history-capture-policy.test.ts +102 -0
  339. package/tests/history-child-guard.test.ts +70 -0
  340. package/tests/history-command-registration.test.ts +164 -0
  341. package/tests/history-dedupe-entries.test.ts +123 -0
  342. package/tests/history-delete-backfill.test.ts +190 -0
  343. package/tests/history-delete-confirm.test.ts +460 -0
  344. package/tests/history-dispatch.test.ts +180 -0
  345. package/tests/history-drain-hidden.test.ts +110 -0
  346. package/tests/history-drain-order.test.ts +98 -0
  347. package/tests/history-expanded-globals.test.ts +62 -0
  348. package/tests/history-gc.test.ts +832 -0
  349. package/tests/history-header-layout.test.ts +265 -0
  350. package/tests/history-hide-prompts.test.ts +275 -0
  351. package/tests/history-lazy-windowing.test.ts +508 -0
  352. package/tests/history-legacy-migrate-v2.test.ts +297 -0
  353. package/tests/history-load-shared-history.test.ts +53 -0
  354. package/tests/history-max-results-cap.test.ts +76 -0
  355. package/tests/history-multi-reader.test.ts +203 -0
  356. package/tests/history-off-path.test.ts +170 -0
  357. package/tests/history-openflow-integration.test.ts +173 -0
  358. package/tests/history-overlay-margin.test.ts +326 -0
  359. package/tests/history-preview-layout.test.ts +93 -0
  360. package/tests/history-registry.test.ts +143 -0
  361. package/tests/history-scope-delete.test.ts +411 -0
  362. package/tests/history-search-caret-keys.test.ts +142 -0
  363. package/tests/history-seed-bootstrap.test.ts +170 -0
  364. package/tests/history-seed-regen.test.ts +129 -0
  365. package/tests/history-selector-windowing.test.ts +94 -0
  366. package/tests/history-session-scan-directory.test.ts +87 -0
  367. package/tests/history-session-scan-extract.test.ts +583 -0
  368. package/tests/history-session-writer.test.ts +351 -0
  369. package/tests/history-store-paths.test.ts +79 -0
  370. package/tests/history-tombstone-exact.test.ts +139 -0
  371. package/tests/history-wheel-mouse.test.ts +242 -0
  372. package/tests/inprocess-reviewer.test.ts +49 -19
  373. package/tests/install-gentle-ai-failure.test.ts +69 -0
  374. package/tests/install-tui-mode-setting.test.ts +15 -0
  375. package/tests/install-wizard.test.ts +1223 -0
  376. package/tests/installer-bundles.test.ts +122 -0
  377. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  378. package/tests/installer-preflight.test.ts +750 -0
  379. package/tests/installer-probes.test.ts +1106 -0
  380. package/tests/installer-runner.test.ts +2158 -0
  381. package/tests/installer-server.test.ts +1567 -0
  382. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  383. package/tests/issue-creation-skill.test.ts +61 -0
  384. package/tests/jobs-view.test.ts +160 -0
  385. package/tests/main-channel-upgrade.test.ts +389 -0
  386. package/tests/main-channel.test.ts +344 -0
  387. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  388. package/tests/model-price-ratio.test.ts +116 -0
  389. package/tests/model-profiles-preservation.test.ts +153 -0
  390. package/tests/model-routing-authority.test.ts +16 -0
  391. package/tests/model-routing-package.test.ts +10 -0
  392. package/tests/nan-provider-runtime.test.ts +156 -0
  393. package/tests/nan-provider.test.ts +481 -0
  394. package/tests/native-binary-gate.test.ts +2 -0
  395. package/tests/native-review-capability-contract.test.ts +13 -1
  396. package/tests/native-review-cli.test.ts +239 -118
  397. package/tests/native-review-consent.test.ts +2 -2
  398. package/tests/native-review-parity-runtime.test.ts +101 -4
  399. package/tests/native-review-parity.test.ts +12 -48
  400. package/tests/native-risk-evidence-parity.test.ts +21 -0
  401. package/tests/no-self-pause-contract.test.ts +87 -0
  402. package/tests/notification-audio-native.test.ts +281 -0
  403. package/tests/notification-audio-windows.test.ts +581 -0
  404. package/tests/notification-audio.test.ts +548 -0
  405. package/tests/notification-customize.test.ts +655 -0
  406. package/tests/notification-events.test.ts +115 -0
  407. package/tests/notification-policy.test.ts +201 -0
  408. package/tests/notification-pulse-client.test.ts +356 -0
  409. package/tests/notification-pulse-protocol.test.ts +213 -0
  410. package/tests/notification-pulse-stream.test.ts +320 -0
  411. package/tests/notification-scheduler.test.ts +258 -0
  412. package/tests/notification-ui.test.ts +157 -0
  413. package/tests/npm-pack-result.test.ts +41 -0
  414. package/tests/odd-integration.test.ts +76 -0
  415. package/tests/odd-phase-inference.test.ts +213 -0
  416. package/tests/odd-phase-loader.test.ts +253 -0
  417. package/tests/odd-phase.test.ts +307 -0
  418. package/tests/odd-routing-canonical-ratchet.test.ts +60 -14
  419. package/tests/odd-routing-contract.test.ts +158 -50
  420. package/tests/orchestrator-budget.test.ts +43 -45
  421. package/tests/orchestrator-catalog.test.ts +146 -0
  422. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  423. package/tests/orchestrator-consultation.test.ts +111 -0
  424. package/tests/orchestrator-discovery.test.ts +241 -0
  425. package/tests/orchestrator-helper-consent.test.ts +171 -0
  426. package/tests/orchestrator-helper.test.ts +297 -0
  427. package/tests/orchestrator-message-card.test.ts +190 -0
  428. package/tests/orchestrator-presence.test.ts +23 -0
  429. package/tests/orchestrator-rdd-ownership.test.ts +8 -7
  430. package/tests/orchestrator-scope.test.ts +107 -0
  431. package/tests/orchestrator-state.test.ts +114 -0
  432. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  433. package/tests/orchestrator-tool-card.test.ts +398 -0
  434. package/tests/orchestrator-work-search.test.ts +318 -0
  435. package/tests/orchestrator-work.test.ts +147 -0
  436. package/tests/overlay-repaint.test.ts +74 -0
  437. package/tests/package-manifest.test.ts +344 -123
  438. package/tests/packed-runner-owned-path.test.ts +80 -0
  439. package/tests/parallel-reason-contract.test.ts +21 -0
  440. package/tests/parallel-writer-admission.test.ts +210 -0
  441. package/tests/permission-mode-policy.test.ts +194 -0
  442. package/tests/persona-single-channel.test.ts +10 -18
  443. package/tests/pi-pretty.test.ts +56 -0
  444. package/tests/provider-defect-handoff.test.ts +7 -14
  445. package/tests/quiet-bash-runtime.test.ts +76 -0
  446. package/tests/quiet-tool-rendering.test.ts +569 -187
  447. package/tests/rdd-aware-verification-contract.test.ts +103 -40
  448. package/tests/rdd-status-line.test.ts +9 -4
  449. package/tests/render-cache-performance.test.ts +48 -0
  450. package/tests/resume-hint-extension.test.ts +122 -0
  451. package/tests/review-agent-end-preflight.test.ts +237 -21
  452. package/tests/review-candidate-owner-retry.test.ts +22 -1
  453. package/tests/review-candidate-view.test.ts +399 -12
  454. package/tests/review-contract-prompt.test.ts +138 -44
  455. package/tests/review-controller-lock-status.test.ts +0 -1
  456. package/tests/review-controller-native-recovery.test.ts +327 -17
  457. package/tests/review-controller-native-routing.test.ts +866 -6
  458. package/tests/review-controller-workspace-root.test.ts +206 -6
  459. package/tests/review-controller.test.ts +2 -2
  460. package/tests/review-correction-lifecycle.test.ts +2 -1
  461. package/tests/review-gate.test.ts +7 -4
  462. package/tests/review-graph-schema.test.ts +2 -2
  463. package/tests/review-host-relay-routing.test.ts +342 -4
  464. package/tests/review-host-relay.test.ts +49 -2
  465. package/tests/review-integration-v2-forward.test.ts +221 -1
  466. package/tests/review-json-arguments.test.ts +167 -0
  467. package/tests/review-last-event-closure.test.ts +157 -8
  468. package/tests/review-ledger-contract.test.ts +14 -37
  469. package/tests/review-policy-judgment-day.test.ts +1 -1
  470. package/tests/review-policy-ordinary.test.ts +2 -2
  471. package/tests/review-relay-transport-agent.test.ts +66 -1
  472. package/tests/review-reminder-receipt.test.ts +47 -1
  473. package/tests/review-risk-assessment.test.ts +594 -26
  474. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  475. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  476. package/tests/review-sidebar-state.test.ts +402 -0
  477. package/tests/review-test-fixtures.ts +7 -1
  478. package/tests/review-transaction.test.ts +18 -1
  479. package/tests/run-test-suite.test.ts +202 -0
  480. package/tests/runtime-harness.mjs +186 -800
  481. package/tests/runtime-metrics-children.test.ts +16 -23
  482. package/tests/runtime-metrics-extension.test.ts +1 -1
  483. package/tests/runtime-metrics-native.test.ts +31 -1
  484. package/tests/selection-engine.test.ts +451 -0
  485. package/tests/session-change-capture.test.ts +10 -10
  486. package/tests/session-changes-shell.test.ts +242 -48
  487. package/tests/session-changes.test.ts +17 -24
  488. package/tests/session-messaging-grants.test.ts +255 -0
  489. package/tests/session-profile-binding.test.ts +163 -0
  490. package/tests/session-profile-freeze.test.ts +381 -0
  491. package/tests/session-subject-routing-contract.test.ts +34 -0
  492. package/tests/session-worktree-registry.test.ts +134 -2
  493. package/tests/shell-bar.test.ts +422 -1
  494. package/tests/shell-card.test.ts +353 -1
  495. package/tests/shell-changes-view.test.ts +135 -32
  496. package/tests/shell-changes.test.ts +61 -62
  497. package/tests/shell-prompt.test.ts +94 -2
  498. package/tests/shell-render-cache-runtime.test.ts +69 -0
  499. package/tests/shell-render-cache.test.ts +129 -0
  500. package/tests/shell-sidebar-layout.test.ts +351 -22
  501. package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
  502. package/tests/shell-todo.test.ts +207 -3
  503. package/tests/shell-usage-view.test.ts +28 -1
  504. package/tests/shell-usage.test.ts +108 -0
  505. package/tests/short-check-routing-contract.test.ts +49 -0
  506. package/tests/skill-registry-watcher.test.ts +119 -0
  507. package/tests/skill-registry.test.ts +357 -2
  508. package/tests/startup-banner.test.ts +246 -4
  509. package/tests/startup-home-repo.test.ts +170 -0
  510. package/tests/stats-collector.test.ts +195 -0
  511. package/tests/stats-view.test.ts +202 -0
  512. package/tests/status-timing-diagnostics.test.ts +283 -0
  513. package/tests/support/orchestrator-modules.ts +19 -0
  514. package/tests/task-size-routing-contract.test.ts +241 -0
  515. package/tests/telemetry-trigger.test.ts +107 -20
  516. package/tests/test-strength-contract.test.ts +124 -0
  517. package/tests/theme-customization.test.ts +72 -0
  518. package/tests/verify-agent-contract.test.ts +90 -0
  519. package/tests/verify-first-launch-contract.test.ts +139 -0
  520. package/tests/verify-package-files.test.ts +56 -0
  521. package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
  522. package/tests/vim-editor-adapter.test.ts +804 -0
  523. package/tests/vim-normal-engine.test.ts +101 -0
  524. package/tests/vim-operator-engine.test.ts +215 -0
  525. package/tests/vim-policy.test.ts +19 -0
  526. package/tests/vim-visual-engine.test.ts +52 -0
  527. package/tests/visual-customization-policy.test.ts +139 -0
  528. package/tests/visual-customize-view.test.ts +696 -0
  529. package/tests/visual-profiles.test.ts +87 -0
  530. package/tests/writer-edit-surface-scope.test.ts +59 -2
  531. package/tests/writer-surfaces.test.ts +199 -0
  532. package/tests/yolo-customize.test.ts +271 -0
  533. package/tests/yolo-mode-runtime.test.ts +188 -0
  534. package/tests/yolo-mode.test.ts +289 -0
  535. package/tests/yolo-session-policy.test.ts +59 -0
  536. package/themes/Gentle.json +2 -1
  537. package/themes/Gentleman-Cute.json +2 -1
  538. package/themes/Gentleman-Sexy.json +2 -1
  539. package/assets/agents/sdd-apply.md +0 -159
  540. package/assets/agents/sdd-archive.md +0 -228
  541. package/assets/agents/sdd-design.md +0 -49
  542. package/assets/agents/sdd-explore.md +0 -48
  543. package/assets/agents/sdd-init.md +0 -56
  544. package/assets/agents/sdd-onboard.md +0 -52
  545. package/assets/agents/sdd-proposal.md +0 -64
  546. package/assets/agents/sdd-remediate.md +0 -37
  547. package/assets/agents/sdd-research.md +0 -49
  548. package/assets/agents/sdd-spec.md +0 -192
  549. package/assets/agents/sdd-status.md +0 -54
  550. package/assets/agents/sdd-tasks.md +0 -108
  551. package/assets/agents/sdd-verify.md +0 -124
  552. package/assets/chains/sdd-full.chain.md +0 -83
  553. package/assets/chains/sdd-plan.chain.md +0 -56
  554. package/assets/chains/sdd-verify.chain.md +0 -43
  555. package/assets/sdd-orchestrator-workflow.md +0 -319
  556. package/assets/support/sdd-status-contract.md +0 -77
  557. package/docs/assets/diagrams/sdd-cycle.svg +0 -14
  558. package/extensions/sdd-init.ts +0 -816
  559. package/lib/openspec-deltas.ts +0 -156
  560. package/lib/sdd-preflight.ts +0 -1066
  561. package/lib/sdd-research-capabilities.ts +0 -94
  562. package/lib/sdd-status.ts +0 -26
  563. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
  564. package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
  565. package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
  566. package/tests/openspec-deltas.test.ts +0 -209
  567. package/tests/sdd-agent-tools.test.ts +0 -156
  568. package/tests/sdd-archive-replay.test.ts +0 -82
  569. package/tests/sdd-classical-continuation.test.ts +0 -74
  570. package/tests/sdd-execution-routing-contract.test.ts +0 -44
  571. package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
  572. package/tests/sdd-native-managed-uptake.test.ts +0 -243
  573. package/tests/sdd-no-attempts-contract.test.ts +0 -15
  574. package/tests/sdd-odd-integration.test.ts +0 -33
  575. package/tests/sdd-optional-research.test.ts +0 -124
  576. package/tests/sdd-planning-routing-contract.test.ts +0 -45
  577. package/tests/sdd-preflight-rpc-input.test.ts +0 -125
  578. package/tests/sdd-preflight.test.ts +0 -541
  579. package/tests/sdd-research-capabilities.test.ts +0 -114
  580. package/tests/sdd-research-live.test.ts +0 -241
  581. package/tests/sdd-selection-transport.test.ts +0 -653
  582. package/tests/sdd-status.test.ts +0 -9
  583. package/tests/sdd-task-truth.test.ts +0 -43
  584. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  585. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  586. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  587. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  588. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  589. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  590. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -6,23 +6,28 @@
6
6
  // process, filesystem, and child process.
7
7
  import {
8
8
  accessSync,
9
+ chmodSync,
9
10
  closeSync,
10
11
  constants as fsConstants,
11
12
  existsSync,
13
+ fstatSync,
14
+ lstatSync,
12
15
  mkdirSync,
16
+ mkdtempSync,
13
17
  openSync,
14
18
  readdirSync,
15
19
  readFileSync,
20
+ readSync,
16
21
  realpathSync,
17
22
  renameSync,
18
23
  rmSync,
19
24
  statSync,
20
25
  writeFileSync,
21
26
  } from "node:fs";
22
- import { createRequire } from "node:module";
23
- import { constants as osConstants, homedir } from "node:os";
27
+ import { constants as osConstants, homedir, tmpdir } from "node:os";
24
28
  import { delimiter, dirname, join, resolve as resolvePath } from "node:path";
25
29
  import { spawn, spawnSync } from "node:child_process";
30
+ import { createHash } from "node:crypto";
26
31
  import { fileURLToPath } from "node:url";
27
32
  import {
28
33
  buildPiInvocation,
@@ -53,8 +58,46 @@ import {
53
58
  restoreJsonField,
54
59
  shellQuote,
55
60
  } from "../runtime/gentle-shell-launcher.mjs";
56
- import { GENTLE_AI_VERSION, gentleAiBinaryPath, PackageLocalGentleAiBinaryMissingError } from "../runtime/gentle-ai-binary.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";
88
+ import {
89
+ parseResumeHandoff,
90
+ planResumeHint,
91
+ RESUME_HANDOFF_DIR_PREFIX,
92
+ RESUME_HANDOFF_ENV,
93
+ RESUME_HANDOFF_FILE,
94
+ } from "../runtime/gentle-shell-resume-hint.mjs";
95
+ import { bundledGentleAiPaths, GENTLE_AI_VERSION, gentleAiBinaryPath, gentleAiIsolationEnv, PackageLocalGentleAiBinaryMissingError } from "../runtime/gentle-ai-binary.mjs";
57
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";
58
101
 
59
102
  const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
60
103
 
@@ -72,17 +115,30 @@ function readJsonIfExists(path) {
72
115
  }
73
116
  }
74
117
 
75
- // @earendil-works/pi-coding-agent ships as an optional peer dependency: it may
76
- // not be installed at all, so a resolution failure here is expected, not an error.
118
+ // Resolve the public ESM entry without importing the agent or reaching through
119
+ // its exports map. Only an absent optional peer permits PATH fallback; malformed
120
+ // installed metadata must not silently select a different runtime.
77
121
  function resolveBundledCli() {
122
+ let publicEntry;
78
123
  try {
79
- const require = createRequire(import.meta.url);
80
- const pkgJsonPath = require.resolve("@earendil-works/pi-coding-agent/package.json");
81
- const cliPath = join(dirname(pkgJsonPath), "dist", "bundle", "cli.js");
82
- return existsSync(cliPath) ? cliPath : undefined;
83
- } catch {
84
- return undefined;
124
+ publicEntry = fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
125
+ } catch (error) {
126
+ if (error.code === "ERR_MODULE_NOT_FOUND") return undefined;
127
+ throw error;
128
+ }
129
+ const entry = realpathSync(publicEntry);
130
+ const root = dirname(dirname(entry));
131
+ const expectedEntry = join(root, "dist", "index.js");
132
+ const metadata = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
133
+ if (entry !== expectedEntry || metadata.name !== "@earendil-works/pi-coding-agent" ||
134
+ metadata.bin?.pi !== "dist/bundle/cli.js") {
135
+ throw new Error("Unsupported adjacent Pi package entry/name/bin metadata");
136
+ }
137
+ const cliPath = join(root, metadata.bin.pi);
138
+ if (realpathSync(cliPath) !== cliPath || !statSync(cliPath).isFile()) {
139
+ throw new Error("Unsupported adjacent Pi CLI path");
85
140
  }
141
+ return cliPath;
86
142
  }
87
143
 
88
144
  function findOnPath(name) {
@@ -107,6 +163,218 @@ function signalExitCode(signal) {
107
163
  return 128 + (typeof number === "number" ? number : 0);
108
164
  }
109
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 read-only in Engram v3.3.2: `engram instance-id` first runs a GitHub
280
+ // update check that sends GH_TOKEN or GITHUB_TOKEN (cmd/engram/main.go:742-744,
281
+ // 835-847; internal/version/check.go:23-24,60-70,171-176) unless
282
+ // ENGRAM_NO_UPDATE_CHECK is set (check.go:19,49-51,114-117). It then resolves the
283
+ // default ~/.engram (main.go:749-765) and runs EnsureInstanceID
284
+ // (internal/store/instance.go:15-79): the data dir, `.instance-id.lock` (waits
285
+ // up to 60s, internal/store/migration_lock.go:29-76) and `.instance-id`. It
286
+ // returns (main.go:767-775) before orphan migration (main.go:779) and opens no
287
+ // database. The timeout stays below that lock wait: a held lock means a
288
+ // concurrent creator, and memory then stays off for the launch.
289
+ const ENGRAM_PROVISION_TIMEOUT_MS = 15000;
290
+
291
+ // A fresh launch's store identity, provisioned by Shell itself with the verified
292
+ // core before Pi starts, then recorded. False when any step fails.
293
+ function provisionEngramStore() {
294
+ const core = verifiedBundledCore();
295
+ if (core === undefined || observeEngramStore().store !== "absent") return false;
296
+ let target;
297
+ try {
298
+ target = join(realpathSync(homedir()), ".engram");
299
+ } catch {
300
+ return false;
301
+ }
302
+ // The provider's own environment, except for this one invocation: no update
303
+ // check and no GitHub tokens. ENGRAM_DATA_DIR is never set here.
304
+ const env = { ...process.env, ENGRAM_NO_UPDATE_CHECK: "1" };
305
+ delete env.GH_TOKEN;
306
+ delete env.GITHUB_TOKEN;
307
+ const result = spawnSync(core, ["instance-id"], { env, encoding: "utf8", timeout: ENGRAM_PROVISION_TIMEOUT_MS, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
308
+ const observed = observeEngramStore();
309
+ if (result.status !== 0 || observed.store !== "present" || observed.dataDir !== target || !/^[a-f0-9]{32}$/.test(observed.instanceId ?? "") ||
310
+ String(result.stdout).trim() !== observed.instanceId) return false;
311
+ return writeEngramProvenance({ schema: 1, dataDir: target, instanceId: observed.instanceId, coreSha256: sha256File(core) });
312
+ }
313
+
314
+ function writeEngramProvenance(record) {
315
+ try {
316
+ const path = engramProvenancePath(bundledInstall.root, process.platform);
317
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
318
+ const temporary = `${path}.${process.pid}.tmp`;
319
+ writeFileSync(temporary, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600, flag: "wx" });
320
+ renameSync(temporary, path);
321
+ return true;
322
+ } catch (error) {
323
+ process.stderr.write(`gentle-shell: could not record Engram provenance (${error.message})\n`);
324
+ return false;
325
+ }
326
+ }
327
+
328
+ // Use Pi's supported configuration-only install command, never an alternate
329
+ // extension loader. Verify advertised enforcement before trusting the policy.
330
+ async function ensureEngramCompanion(home, runtime, stdio, timeoutMs) {
331
+ const metadata = join(home.dir, "npm", "node_modules", "gentle-engram", "package.json");
332
+ let packages;
333
+ try {
334
+ packages = JSON.parse(readJsonIfExists(join(home.dir, "settings.json")) ?? "{}").packages ?? [];
335
+ } catch {
336
+ return { ok: false, exitCode: 1, message: "gentle-shell: cannot verify Engram startupPolicy from malformed settings; repair settings and recheck" };
337
+ }
338
+ if (!Array.isArray(packages)) return { ok: false, exitCode: 1, message: "gentle-shell: cannot verify Engram startupPolicy from non-array packages" };
339
+ let pinned = false;
340
+ let local = false;
341
+ let npmSelected = false;
342
+ for (const entry of packages) {
343
+ const source = typeof entry === "string" ? entry : entry?.source;
344
+ if (source === ENGRAM_COMPANION_SOURCE) pinned = true;
345
+ if (typeof source === "string" && /^npm:gentle-engram(?:@|$)/.test(source)) npmSelected = true;
346
+ if (typeof source !== "string" || source.startsWith("npm:")) continue;
347
+ // Preserve an explicit local companion selection rather than overwrite it.
348
+ const selected = resolvePath(home.dir, source);
349
+ if (readPackageName(selected) !== "gentle-engram") continue;
350
+ if (!supportsEngramStartupPolicy(readJsonIfExists(join(selected, "package.json")))) {
351
+ return { ok: false, exitCode: 1, message: `gentle-shell: selected companion ${source} lacks engram.startupPolicy=1; update it and recheck (selection preserved)` };
352
+ }
353
+ local = true;
354
+ }
355
+ if (local && !npmSelected) return { ok: true };
356
+ if (pinned && supportsEngramStartupPolicy(readJsonIfExists(metadata))) return { ok: true };
357
+ process.stderr.write(`gentle-shell: installing policy-capable companion ${ENGRAM_COMPANION_SOURCE} through Pi\n`);
358
+ const result = await spawnAndWait(runtime.command, [...runtime.args, "install", ENGRAM_COMPANION_SOURCE], buildSetupEnv(home, runtime), stdio, timeoutMs);
359
+ let declared = false;
360
+ try {
361
+ declared = JSON.parse(readJsonIfExists(join(home.dir, "settings.json")) ?? "{}").packages?.some((entry) =>
362
+ (typeof entry === "string" ? entry : entry?.source) === ENGRAM_COMPANION_SOURCE) === true;
363
+ } catch { /* Missing or malformed readback cannot establish enforcement. */ }
364
+ if (!result.ok || result.timedOut || !declared || !supportsEngramStartupPolicy(readJsonIfExists(metadata))) {
365
+ 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." };
366
+ }
367
+ return { ok: true };
368
+ }
369
+
370
+ // The bundled children's environment (bundledChildEnv), with the pinned Engram
371
+ // only once it is published: computed again right before Pi starts, so an
372
+ // Engram the automatic setup just published is used in the same launch.
373
+ function bundledProcessEnv() {
374
+ return bundledChildEnv(process.env, { node: process.execPath, cli: bundledCli, prefix: bundledInstall.root, engram: verifiedBundledCore(),
375
+ guard: ENGRAM_GUARD, memory: bundledMemory });
376
+ }
377
+
110
378
  function ownPackageVersion() {
111
379
  const packageJson = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
112
380
  return packageJson.version;
@@ -162,6 +430,45 @@ function isDirectory(path) {
162
430
  }
163
431
  }
164
432
 
433
+ // Reuse Herdr's managed bridge, never its transport. This process-boundary
434
+ // lookup is deliberately best-effort and does not modify either agent home.
435
+ function managedHerdrExtensionArgs(home, args) {
436
+ const env = process.env;
437
+ if (home.mode !== "isolated" || args.piSubcommand !== undefined || args.passthrough[0] === "mcp") return [];
438
+ if (env.HERDR_ENV !== "1" || !env.HERDR_SOCKET_PATH?.trim() || !env.HERDR_PANE_ID?.trim()) return [];
439
+ if (env.GENTLE_PI_AGENTS_CHILD === "1" || !process.stdin.isTTY || !process.stdout.isTTY) return [];
440
+ // Only automatic interactive loading: a user opt-out must not become an
441
+ // explicit -e (which Pi loads even under --no-extensions). Conservatively
442
+ // skip ambiguous mode flags too; --mode text alone still allows a TUI.
443
+ const forwarded = args.passthrough;
444
+ if (forwarded.some((arg) => ["--no-extensions", "-ne", "--print", "-p", "--export", "--list-models", "-v"].includes(arg))) return [];
445
+ if (forwarded.some((arg, i) => (arg === "--mode" && forwarded[i + 1] !== "text") || arg.startsWith("--mode="))) return [];
446
+ try {
447
+ if (!statSync(env.HERDR_SOCKET_PATH).isSocket()) return [];
448
+ } catch {
449
+ return [];
450
+ }
451
+ // Prefer a bridge in the selected home over adding a competing copy; keep
452
+ // the incoming Pi home override before falling back to Herdr's usual home.
453
+ const agentHomes = [home.dir, env.PI_CODING_AGENT_DIR, join(homedir(), ".pi", "agent")];
454
+ for (const agentHome of agentHomes) {
455
+ if (!agentHome) continue;
456
+ const bridge = join(agentHome, "extensions", "herdr-agent-state.ts");
457
+ try {
458
+ if (!statSync(bridge).isFile()) continue;
459
+ accessSync(bridge, fsConstants.R_OK);
460
+ // Pi's package-manager.toResolvedPaths and resource-loader.mergePaths
461
+ // dedupe canonical files across discovery, explicit -e and manifests.
462
+ // Existence alone is NOT proof of loading: declare the resource and
463
+ // let that resolver dedupe it, including explicit aliases from argv.
464
+ return ["-e", realpathSync(bridge)];
465
+ } catch {
466
+ // An absent/unreadable bridge never prevents the ordinary launch.
467
+ }
468
+ }
469
+ return [];
470
+ }
471
+
165
472
  // Real-fs adapter for discoverLooseExtensionEntries (lib/gentle-shell-launcher.ts):
166
473
  // statSync-based isFile/isDirectory (not readdirSync's Dirent, which uses
167
474
  // lstat and so would treat a symlinked file or directory as neither) so a
@@ -254,9 +561,46 @@ function readRawConfig(configPath) {
254
561
  function writeRawConfig(configPath, config) {
255
562
  const configDir = dirname(configPath);
256
563
  if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true, mode: 0o700 });
257
- const tempPath = join(configDir, `.${basenameOf(configPath)}.gentle-shell-${process.pid}.tmp`);
258
- writeFileSync(tempPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
259
- renameSync(tempPath, configPath);
564
+ writeFileAtomically(configPath, `${JSON.stringify(config, null, 2)}\n`);
565
+ }
566
+
567
+ // The one atomic write every launcher-owned config/settings update goes
568
+ // through: a temp file next to the real target, then a rename onto it, so a
569
+ // crash or kill never leaves a partial file. When `path` is a symlink (Nix
570
+ // home-manager, stow, and similar dotfile managers ship settings.json and
571
+ // config.json that way), the write lands on the link's real target and the
572
+ // link itself stays in place; renaming onto `path` would replace the link
573
+ // with a regular file. A missing path or a dangling link has no real target
574
+ // to preserve, so it is written at `path` itself, exactly as before this
575
+ // helper existed, instead of creating a file wherever a dangling link points.
576
+ // The permission bits are `mode` when given, otherwise the real target's own
577
+ // (applied with chmod, so the umask cannot narrow them); a new file keeps
578
+ // the default creation mode. The temp file is removed when the write or
579
+ // rename throws, and the error propagates to the caller's own handling.
580
+ function writeFileAtomically(path, data, { mode } = {}) {
581
+ let target = path;
582
+ try {
583
+ target = realpathSync(path);
584
+ } catch (error) {
585
+ if (error.code !== "ENOENT") throw error;
586
+ }
587
+ let targetMode = mode;
588
+ if (targetMode === undefined) {
589
+ try {
590
+ targetMode = statSync(target).mode & 0o777;
591
+ } catch (error) {
592
+ if (error.code !== "ENOENT") throw error;
593
+ }
594
+ }
595
+ const tempPath = join(dirname(target), `.${basenameOf(target)}.gentle-shell-${process.pid}.tmp`);
596
+ try {
597
+ writeFileSync(tempPath, data);
598
+ if (targetMode !== undefined) chmodSync(tempPath, targetMode);
599
+ renameSync(tempPath, target);
600
+ } catch (error) {
601
+ rmSync(tempPath, { force: true });
602
+ throw error;
603
+ }
260
604
  }
261
605
 
262
606
  function loadConfig() {
@@ -453,15 +797,50 @@ function ensurePackageLocalGentleAi(binaryPath, pinnedVersion, stdio) {
453
797
  // home, and the resolved pi runtime's directory is prepended to PATH so
454
798
  // gentle-ai's (or pi's own) preflight finds `pi` even when it is bundled or
455
799
  // given through GENTLE_SHELL_PI.
456
- function buildSetupEnv(home, runtime) {
800
+ function buildSetupEnv(home, runtime, piLauncherDir) {
801
+ // Bundled: our node, then the version's `pi`, ahead of the user's PATH, and
802
+ // Gentle AI's state, persona and Engram inside the prefix (S9).
803
+ if (bundledEnv) {
804
+ const env = bundledSetupEnv({ ...bundledEnv, PI_CODING_AGENT_DIR: home.dir, GENTLE_PI_AGENT_HOME: home.dir }, { dirs: bundledSetupDirs, platform: process.platform });
805
+ return gentleAiIsolationEnv(env, { packageRoot });
806
+ }
457
807
  return {
458
808
  ...process.env,
459
809
  PI_CODING_AGENT_DIR: home.dir,
460
810
  GENTLE_PI_AGENT_HOME: home.dir,
461
- PATH: `${dirname(runtime.command)}${delimiter}${process.env.PATH ?? ""}`,
811
+ PATH: [...(piLauncherDir ? [piLauncherDir] : []), dirname(runtime.command), process.env.PATH ?? ""].join(delimiter),
462
812
  };
463
813
  }
464
814
 
815
+ // Gentle AI requires a `pi` on its PATH and runs `pi install npm:gentle-pi`
816
+ // with it. A Pi gentle-shell resolved elsewhere (the adjacent package, run
817
+ // as `node <cli>`, or GENTLE_SHELL_PI) is not that `pi`, so a first launch
818
+ // from a terminal without one failed every time. This writes a private,
819
+ // temporary `pi` that runs exactly the resolved runtime; null when the
820
+ // runtime already is the `pi` on PATH (or for a bundled install, whose
821
+ // setup PATH already starts with the version's node_modules/.bin).
822
+ function writeSetupPiLauncher(runtime) {
823
+ if (bundledEnv || runtime.kind === "path") return null;
824
+ let dir;
825
+ try {
826
+ dir = mkdtempSync(join(tmpdir(), "gentle-shell-pi-"));
827
+ } catch {
828
+ // No usable temp folder: setup runs as it did before, without this `pi`.
829
+ return null;
830
+ }
831
+ if (process.platform === "win32") {
832
+ // Batch expands % even inside quotes; double it so a path stays literal.
833
+ const quote = (value) => `"${value.replaceAll("%", "%%")}"`;
834
+ // A batch runtime needs `call`, or control never returns for `exit /b`.
835
+ const call = /\.(?:cmd|bat)$/i.test(runtime.command) ? "call " : "";
836
+ writeFileSync(join(dir, "pi.cmd"), `@echo off\r\n${call}${[runtime.command, ...runtime.args].map(quote).join(" ")} %*\r\nexit /b %errorlevel%\r\n`);
837
+ } else {
838
+ const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
839
+ writeFileSync(join(dir, "pi"), `#!/bin/sh\nexec ${[runtime.command, ...runtime.args].map(quote).join(" ")} "$@"\n`, { mode: 0o700 });
840
+ }
841
+ return dir;
842
+ }
843
+
465
844
  // The shared Pi persona file gentle-ai writes on every install, regardless
466
845
  // of the target home: its own PiPersonaConfigPath always resolves against
467
846
  // the OS home, never PI_CODING_AGENT_DIR (gentle-ai internal/components/persona/inject.go),
@@ -473,6 +852,29 @@ function sharedPersonaPath() {
473
852
  return join(homedir(), ".pi", "gentle-ai", "persona.json");
474
853
  }
475
854
 
855
+ // The persona files a setup run may rewrite. Bundled (S9): the prefix's own,
856
+ // which a Gentle AI with GENTLE_AI_PI_CONFIG_DIR (gentle-ai #5462) writes, and
857
+ // still the user's, which a pinned Gentle AI without it writes; restoring an
858
+ // unchanged file does nothing.
859
+ function setupPersonaPaths() {
860
+ if (!bundledInstall) return [sharedPersonaPath()];
861
+ return [join(bundledGentleAiPaths(bundledInstall.root, process.platform).piConfig, "persona.json"), sharedPersonaPath()];
862
+ }
863
+
864
+ // Bundled setup (S9): the pinned Engram GENTLE_AI_ENGRAM_BIN names, published
865
+ // once in runtime/ (verified, never brew or the user's PATH). A failure stops
866
+ // setup rather than let Gentle AI acquire another Engram.
867
+ async function ensureBundledEngram() {
868
+ const { ensureEngram, prefixLayout } = await import("../scripts/bundled-install.mjs");
869
+ const layout = prefixLayout({ platform: process.platform, env: process.env, home: homedir(), root: bundledInstall.root });
870
+ try {
871
+ await ensureEngram({ layout, platform: process.platform, arch: process.arch, env: process.env });
872
+ return { ok: true };
873
+ } catch (error) {
874
+ return { ok: false, exitCode: 1, message: `gentle-shell: could not install the pinned Engram in ${layout.engramDir} (${error.message})` };
875
+ }
876
+ }
877
+
476
878
  // Records `path`'s current state before a child process that might rewrite
477
879
  // it runs: whether it exists, and if so its exact bytes and mode. Returns
478
880
  // `{ path, existed: false }` for a missing file so restoreFile below knows
@@ -512,9 +914,7 @@ function restoreFile(snapshot) {
512
914
  }
513
915
  if (currentBytes !== undefined && currentBytes.equals(snapshot.bytes)) return false;
514
916
  mkdirSync(dirname(path), { recursive: true });
515
- const tempPath = join(dirname(path), `.${basenameOf(path)}.gentle-shell-restore-${process.pid}.tmp`);
516
- writeFileSync(tempPath, snapshot.bytes, { mode: snapshot.mode });
517
- renameSync(tempPath, path);
917
+ writeFileAtomically(path, snapshot.bytes, { mode: snapshot.mode });
518
918
  return true;
519
919
  }
520
920
 
@@ -574,10 +974,7 @@ function restoreManagedAssetDigestField(path, originalText) {
574
974
  if (currentText === undefined) return false;
575
975
  const restoredText = restoreJsonField(originalText, currentText, MANAGED_ASSET_DIGEST_FIELD);
576
976
  if (restoredText === undefined) return false;
577
- const mode = statSync(path).mode & 0o777;
578
- const tempPath = join(dirname(path), `.${basenameOf(path)}.gentle-shell-restore-${process.pid}.tmp`);
579
- writeFileSync(tempPath, restoredText, { mode });
580
- renameSync(tempPath, path);
977
+ writeFileAtomically(path, restoredText);
581
978
  return true;
582
979
  }
583
980
 
@@ -618,10 +1015,7 @@ function enforceDefaultThemeField(settingsPath, originalSettingsText) {
618
1015
  ? restoreJsonField(originalSettingsText, currentText, "theme")
619
1016
  : forceJsonFieldIfAbsentInOriginal(originalSettingsText, currentText, "theme", DEFAULT_THEME_NAME);
620
1017
  if (newText === undefined) return false;
621
- const mode = statSync(settingsPath).mode & 0o777;
622
- const tempPath = join(dirname(settingsPath), `.${basenameOf(settingsPath)}.gentle-shell-restore-${process.pid}.tmp`);
623
- writeFileSync(tempPath, newText, { mode });
624
- renameSync(tempPath, settingsPath);
1018
+ writeFileAtomically(settingsPath, newText);
625
1019
  return originalHadTheme ? "restored" : "forced";
626
1020
  }
627
1021
 
@@ -641,6 +1035,53 @@ function safely(label, path, fallback, fn) {
641
1035
  }
642
1036
  }
643
1037
 
1038
+ // gentle-pi replaces Pi's replaceable builtin codemode with its own decorated
1039
+ // codemode tool (extensions/quiet-tools.ts -> registerCompactCodemode in
1040
+ // lib/codemode-renderer.ts), and Pi prints a startup warning whenever a
1041
+ // builtin loses its tool to another extension. Pi's only per-builtin opt-out
1042
+ // is a `-builtin:<name>` entry in the settings `extensions` array, so every
1043
+ // normal launch ensures that entry in the settings.json of a home
1044
+ // gentle-shell owns (see main, below).
1045
+ const BUILTIN_CODEMODE_EXTENSION = "builtin:codemode";
1046
+
1047
+ // Pure: returns `settingsText` with `-<builtin>` appended to its `extensions`
1048
+ // array (created when absent), or undefined when nothing should change — the
1049
+ // text does not parse as a JSON object, `extensions` exists but is not an
1050
+ // array, or the array already holds any explicit entry for the builtin
1051
+ // (`+`, `-`, `!`, or bare), which is the user's own decision. Keeps every
1052
+ // other key and entry in place, plus the text's own indentation and trailing
1053
+ // newline (same detection as detectJsonFormatting in
1054
+ // lib/gentle-shell-launcher.ts).
1055
+ function withBuiltinExtensionExcluded(settingsText, builtin) {
1056
+ let settings;
1057
+ try {
1058
+ settings = JSON.parse(settingsText);
1059
+ } catch {
1060
+ return undefined;
1061
+ }
1062
+ if (typeof settings !== "object" || settings === null || Array.isArray(settings)) return undefined;
1063
+ const extensions = Object.prototype.hasOwnProperty.call(settings, "extensions") ? settings.extensions : [];
1064
+ if (!Array.isArray(extensions)) return undefined;
1065
+ if (extensions.some((entry) => typeof entry === "string" && entry.replace(/^[+!-]/, "") === builtin)) return undefined;
1066
+ const indent = settingsText.match(/\{\r?\n([ \t]+)/)?.[1];
1067
+ const serialized = JSON.stringify({ ...settings, extensions: [...extensions, `-${builtin}`] }, null, indent);
1068
+ return settingsText.endsWith("\n") ? `${serialized}\n` : serialized;
1069
+ }
1070
+
1071
+ // Applies withBuiltinExtensionExcluded to an existing settings.json with the
1072
+ // same atomic temp-file-then-rename write as the restores above. A missing
1073
+ // settings.json is left missing: only the brand-new-home bootstrap
1074
+ // (installIsolatedTuiModeSetting) seeds that file, and this launch-time step
1075
+ // never takes over that role. Returns true when it actually wrote the file.
1076
+ function ensureBuiltinCodemodeExcluded(settingsPath) {
1077
+ const currentText = readJsonIfExists(settingsPath);
1078
+ if (currentText === undefined) return false;
1079
+ const newText = withBuiltinExtensionExcluded(currentText, BUILTIN_CODEMODE_EXTENSION);
1080
+ if (newText === undefined) return false;
1081
+ writeFileAtomically(settingsPath, newText);
1082
+ return true;
1083
+ }
1084
+
644
1085
  // Provisions `home` with everything `gentle-ai install --agent pi` installs
645
1086
  // into a regular Pi, by spawning the package-local pinned gentle-ai binary
646
1087
  // (never a PATH `gentle-ai`) with PI_CODING_AGENT_DIR/GENTLE_PI_AGENT_HOME set
@@ -682,10 +1123,24 @@ async function runSetupFlow(home, runtime, { dryRun, stdio, timeoutMs }) {
682
1123
 
683
1124
  process.stderr.write(`gentle-shell: provisioning ${home.dir} with the gentle-ai companion packages\n`);
684
1125
 
1126
+ if (bundledInstall && !dryRun) {
1127
+ const engram = await ensureBundledEngram();
1128
+ if (!engram.ok) return engram;
1129
+ }
1130
+ // The memory decision never skips setup: it only selects the Engram
1131
+ // environment (core or guard, policy, ENGRAM_URL) setup's children and the
1132
+ // launched Pi get. Gentle AI's `install --agent pi` and `pi-engram init` open
1133
+ // no store. Reclassified now that the verified core may just be published.
1134
+ if (bundledInstall) {
1135
+ bundledMemory = await classifyBundledMemory();
1136
+ bundledEnv = bundledProcessEnv();
1137
+ if (bundledMemory.notice) process.stderr.write(`gentle-shell: ${bundledMemory.notice}\n`);
1138
+ }
1139
+
685
1140
  const setupArgs = ["install", "--agent", "pi", "--scope", "global", ...(dryRun ? ["--dry-run"] : [])];
686
- const env = buildSetupEnv(home, runtime);
687
- const personaPath = sharedPersonaPath();
688
- const personaSnapshot = safely("snapshot your Pi persona file", personaPath, undefined, () => snapshotFile(personaPath));
1141
+ const piLauncherDir = writeSetupPiLauncher(runtime);
1142
+ const env = buildSetupEnv(home, runtime, piLauncherDir);
1143
+ const personaSnapshots = setupPersonaPaths().map((path) => [path, safely("snapshot your Pi persona file", path, undefined, () => snapshotFile(path))]);
689
1144
  const statePath = sharedGentleAiStatePath();
690
1145
  const originalStateText = safely("read your Gentle AI state file", statePath, undefined, () => readParsableJsonText(statePath));
691
1146
  // Theme restore (never for --link — that home is the user's own
@@ -702,8 +1157,11 @@ async function runSetupFlow(home, runtime, { dryRun, stdio, timeoutMs }) {
702
1157
  try {
703
1158
  installResult = await spawnAndWait(binaryPath, setupArgs, env, stdio, timeoutMs);
704
1159
  } finally {
705
- if (personaSnapshot !== undefined && safely("restore your Pi persona file", personaPath, false, () => restoreFile(personaSnapshot))) {
706
- process.stderr.write(`gentle-shell: kept your Pi persona unchanged (gentle-ai rewrote ${personaPath}; tracked upstream)\n`);
1160
+ if (piLauncherDir) rmSync(piLauncherDir, { recursive: true, force: true });
1161
+ for (const [personaPath, personaSnapshot] of personaSnapshots) {
1162
+ if (personaSnapshot !== undefined && safely("restore your Pi persona file", personaPath, false, () => restoreFile(personaSnapshot))) {
1163
+ process.stderr.write(`gentle-shell: kept your Pi persona unchanged (gentle-ai rewrote ${personaPath}; tracked upstream)\n`);
1164
+ }
707
1165
  }
708
1166
  if (safely("restore your Gentle AI managed-asset record", statePath, false, () => restoreManagedAssetDigestField(statePath, originalStateText))) {
709
1167
  process.stderr.write(
@@ -725,7 +1183,63 @@ async function runSetupFlow(home, runtime, { dryRun, stdio, timeoutMs }) {
725
1183
  }
726
1184
  if (!installResult.ok) return installResult;
727
1185
 
728
- return runPostInstallCleanup(home, runtime, dryRun, stdio, timeoutMs);
1186
+ const cleanupResult = await runPostInstallCleanup(home, runtime, dryRun, stdio, timeoutMs);
1187
+ if (cleanupResult.ok && !dryRun) {
1188
+ warnDisabledMCP(home);
1189
+ // The guard and policy protect memory, so the companion check only warns.
1190
+ const companion = bundledInstall ? await ensureEngramCompanion(home, runtime, stdio, timeoutMs) : { ok: true };
1191
+ if (!companion.ok) process.stderr.write(`${companion.message}\n`);
1192
+ }
1193
+ return cleanupResult;
1194
+ }
1195
+
1196
+ function mcpAdapterActivity(packages) {
1197
+ if (typeof packages === "string") return packages === "npm:pi-mcp-adapter" || packages.startsWith("npm:pi-mcp-adapter@") ? "active" : "inactive";
1198
+ if (Array.isArray(packages)) {
1199
+ const activities = packages.map(mcpAdapterActivity);
1200
+ return activities.includes("active") ? "active" : activities.includes("unknown") ? "unknown" : "inactive";
1201
+ }
1202
+ if (packages === null || typeof packages !== "object") return "inactive";
1203
+ if (typeof packages.source !== "string") return mcpAdapterActivity(Object.keys(packages));
1204
+ if (mcpAdapterActivity(packages.source) === "inactive") return "inactive";
1205
+ const patterns = packages.extensions;
1206
+ if (patterns === undefined) return packages.autoload === false ? "inactive" : "active";
1207
+ if (!Array.isArray(patterns)) return "unknown";
1208
+ if (patterns.length === 0) return "inactive";
1209
+ if (packages.autoload === false) {
1210
+ return patterns.some(pattern => typeof pattern === "string" && !pattern.startsWith("!") && !pattern.startsWith("-")) ? "unknown" : "inactive";
1211
+ }
1212
+ // Ordinary exclusions run after includes, but exact +path entries can
1213
+ // restore resources. Other filters need actual package files to resolve.
1214
+ if (patterns.includes("!**") && !patterns.some(pattern => typeof pattern === "string" && pattern.startsWith("+"))) return "inactive";
1215
+ return "unknown";
1216
+ }
1217
+
1218
+ // Onboarding cannot prove who disabled MCP: diagnose, never re-enable servers.
1219
+ // This also covers automatic provisioning because both routes share setup.
1220
+ function warnDisabledMCP(home) {
1221
+ const settingsPath = join(home.dir, "settings.json");
1222
+ const mcpPath = join(home.dir, "mcp.json");
1223
+ let inspecting = settingsPath;
1224
+ try {
1225
+ const settingsText = readJsonIfExists(settingsPath);
1226
+ if (settingsText === undefined) return;
1227
+ const settings = JSON.parse(settingsText);
1228
+ if (!Array.isArray(settings?.extensions) || !settings.extensions.includes("-builtin:mcp")) return;
1229
+ const activity = mcpAdapterActivity(settings.packages);
1230
+ if (activity === "active") return;
1231
+ inspecting = mcpPath;
1232
+ const mcpText = readJsonIfExists(mcpPath);
1233
+ if (mcpText === undefined) return;
1234
+ const servers = JSON.parse(mcpText)?.mcpServers;
1235
+ if (servers === null || typeof servers !== "object" || Array.isArray(servers) || Object.keys(servers).length === 0) return;
1236
+ const diagnosis = activity === "unknown"
1237
+ ? `pi-mcp-adapter activity cannot be confirmed from its extension filters, so servers in ${mcpPath} may not load`
1238
+ : `pi-mcp-adapter is absent or inactive, so servers in ${mcpPath} will not load`;
1239
+ 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`);
1240
+ } catch (error) {
1241
+ process.stderr.write(`gentle-shell: WARNING: Pi MCP configuration could not be inspected in ${inspecting}: ${error.message}; inspect or repair it manually.\n`);
1242
+ }
729
1243
  }
730
1244
 
731
1245
  // The stderr line printed once `source` is actually removed from `home`.
@@ -805,7 +1319,11 @@ async function removePostInstallSources(sources, index, home, runtime, stdio, ti
805
1319
  // --dry-run, runs the shared flow with the child's stdio inherited (today's
806
1320
  // behavior, unchanged), then exits with its result — this is the one place
807
1321
  // that keeps the pre-S7 exit semantics `handleSetupCommand` always had.
808
- async function handleSetupCommand(commandArgs, home, runtime) {
1322
+ // A successful real run records the home exactly as auto-provisioning does
1323
+ // (#2106: both installers finish with this command, and the first launch
1324
+ // must not install the packages again), but only for a home auto-provisioning
1325
+ // itself would own: never pi's own default home or an unrelated directory.
1326
+ async function handleSetupCommand(commandArgs, home, runtime, { homeHadContentBeforeBootstrap }) {
809
1327
  let dryRun = false;
810
1328
  for (const arg of commandArgs) {
811
1329
  if (arg === "--dry-run") {
@@ -817,6 +1335,19 @@ async function handleSetupCommand(commandArgs, home, runtime) {
817
1335
 
818
1336
  const result = await runSetupFlow(home, runtime, { dryRun, stdio: "inherit" });
819
1337
  if (!result.ok && result.message !== undefined) process.stderr.write(`${result.message}\n`);
1338
+ if (result.ok && !dryRun && (home.mode === "isolated" || home.mode === "path")) {
1339
+ const configPath = resolveConfigPath();
1340
+ const homeKey = safeRealpath(home.dir);
1341
+ const config = readRawConfig(configPath);
1342
+ if (!homeIsForeign(home, provisionedEntry(config, homeKey), homeHadContentBeforeBootstrap)) {
1343
+ try {
1344
+ writeRawConfig(configPath, recordProvisioned(config, homeKey, resolveSetupGentleAiPin(), ownPackageVersion(), new Date().toISOString()));
1345
+ } catch (error) {
1346
+ // Setup itself succeeded; the next launch only sets the home up again.
1347
+ process.stderr.write(`gentle-shell: could not record the setup in ${configPath} (${error.message})\n`);
1348
+ }
1349
+ }
1350
+ }
820
1351
  process.exit(result.exitCode);
821
1352
  }
822
1353
 
@@ -1043,29 +1574,268 @@ async function maybeAutoProvisionHome(home, runtime, { homeHadContentBeforeBoots
1043
1574
  }
1044
1575
  }
1045
1576
 
1577
+ // The adapters `upgrade` and `self-uninstall` run npm and pnpm with: { fs, which, run, invocation? }.
1578
+ async function packageManagerAdapters() {
1579
+ const { hostAdapters } = await import("../scripts/installer-probes.mjs");
1580
+ const { upgradeInvocation } = await import("../scripts/installer-runner.mjs");
1581
+ const { run, fs: probeFs } = hostAdapters();
1582
+ // Windows: npm and pnpm are .cmd shims; run what they run, never through cmd.exe.
1583
+ const invocation = upgradeInvocation({ platform: process.platform, env: process.env, run, fs: probeFs });
1584
+ return {
1585
+ fs: await import("node:fs/promises"),
1586
+ which: async (name) => findOnPath(name) ?? null,
1587
+ ...(invocation ? { invocation } : {}),
1588
+ run: (command, argv, options = {}) => run(command, argv, { env: options.env ?? process.env, cwd: options.cwd, deadlineMs: options.deadlineMs ?? 20 * 60_000 }),
1589
+ };
1590
+ }
1591
+
1592
+ // `gentle-shell upgrade`: runs before any Pi runtime check, since it may replace this package.
1593
+ // On release, a global pnpm or npm install moves to the bundled one when the
1594
+ // latest release publishes its assets (S4), with one question about the old package.
1595
+ async function handleUpgradeCommand(commandArgs) {
1596
+ const { MainChannelError, runUpgrade } = await import("../scripts/main-channel.mjs");
1597
+ const { fetchDistribution, migrateGlobalShell, windowsPathRegistry } = await import("../scripts/bundled-wizard.mjs");
1598
+ const { askLine } = await import("../runtime/gentle-shell-uninstall.mjs");
1599
+ const managers = await packageManagerAdapters();
1600
+ const out = (line) => process.stdout.write(`${line}\n`);
1601
+ const migrate = ({ latest, owner }) => migrateGlobalShell({
1602
+ latest,
1603
+ currentVersion: ownPackageVersion(),
1604
+ platform: process.platform,
1605
+ arch: process.arch,
1606
+ env: process.env,
1607
+ home: homedir(),
1608
+ interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
1609
+ // End of input or Ctrl+C answers No.
1610
+ ask: (question) => askLine(question, { input: process.stdin, output: process.stdout }).catch(() => ""),
1611
+ out,
1612
+ adapters: { owner, distribution: (version) => fetchDistribution({ version }), run: managers.run,
1613
+ ...(process.platform === "win32" ? { registry: windowsPathRegistry(process.env) } : {}) },
1614
+ });
1615
+ try {
1616
+ process.exitCode = await runUpgrade({
1617
+ args: commandArgs,
1618
+ ctx: { env: process.env, home: homedir() },
1619
+ platform: process.platform,
1620
+ packageRoot,
1621
+ currentVersion: ownPackageVersion(),
1622
+ adapters: { fetch: globalThis.fetch, ...managers, migrate },
1623
+ out,
1624
+ });
1625
+ } catch (error) {
1626
+ if (error instanceof MainChannelError) fail(`gentle-shell upgrade: ${error.message}`, error.code === "upgrade-usage" ? 2 : 1);
1627
+ throw error;
1628
+ }
1629
+ }
1630
+
1631
+ // `gentle-shell upgrade` in a bundled install: the latest release installed
1632
+ // beside the active version, verified, then `current` switched (two versions
1633
+ // kept), or `--rollback` to the kept one. Returns the exit code (a failure is
1634
+ // printed, not thrown, so `update --all` can go on); a failure leaves `current`
1635
+ // as it was.
1636
+ async function handleBundledUpgrade(bundled, commandArgs) {
1637
+ const parsed = parseBundledUpgradeArgs(commandArgs);
1638
+ if ("error" in parsed) fail(`gentle-shell upgrade: ${parsed.error}`, parsed.usage ? 2 : 1);
1639
+ const { prefixLayout, rollbackBundled, upgradeBundled } = await import("../scripts/bundled-install.mjs");
1640
+ const layout = prefixLayout({ platform: process.platform, env: process.env, home: homedir(), root: bundled.root });
1641
+ const out = (line) => process.stdout.write(`${line}\n`);
1642
+ try {
1643
+ if (parsed.action === "rollback") {
1644
+ const { from, to } = await rollbackBundled({ layout, home: homedir(), platform: process.platform, arch: process.arch, env: process.env });
1645
+ out(`Switched Gentle Shell from ${from} to ${to}.`);
1646
+ return 0;
1647
+ }
1648
+ const { fetchDistribution } = await import("../scripts/bundled-wizard.mjs");
1649
+ const { latestRelease } = await import("../scripts/main-channel.mjs");
1650
+ await upgradeBundled({
1651
+ layout,
1652
+ home: homedir(),
1653
+ platform: process.platform,
1654
+ arch: process.arch,
1655
+ env: process.env,
1656
+ adapters: { latest: () => latestRelease(globalThis.fetch), distribution: (version) => fetchDistribution({ version }) },
1657
+ out,
1658
+ });
1659
+ return 0;
1660
+ } catch (error) {
1661
+ process.stderr.write(`gentle-shell upgrade: ${error.message}${error.stderr ? `\n${error.stderr}` : ""}\n`);
1662
+ return 1;
1663
+ }
1664
+ }
1665
+
1666
+ // The bundled npm as the `npmCommand` of a home Gentle Shell owns (the isolated
1667
+ // home, or a --home it created or provisioned; never --link or a foreign home),
1668
+ // so Pi installs packages with our npm while its tools keep the user's PATH.
1669
+ // Only that key is written, and only when it differs.
1670
+ async function ensureBundledNpmCommand(bundled, home, homeHadContentBeforeBootstrap) {
1671
+ if (home.mode === "link") return;
1672
+ const previous = provisionedEntry(readRawConfig(resolveConfigPath()), safeRealpath(home.dir));
1673
+ if (homeIsForeign(home, previous, homeHadContentBeforeBootstrap) || !isDirectory(home.dir)) return;
1674
+ const { bundledNpmCommand, ensureNpmShim, prefixLayout } = await import("../scripts/bundled-install.mjs");
1675
+ const layout = prefixLayout({ platform: process.platform, env: process.env, home: homedir(), root: bundled.root });
1676
+ // The entry npmCommand names, also for a runtime installed before it existed.
1677
+ ensureNpmShim(layout);
1678
+ const settingsPath = join(home.dir, "settings.json");
1679
+ const edit = withBundledNpmCommand(readJsonIfExists(settingsPath), bundledNpmCommand(layout));
1680
+ if (edit.status === "malformed") {
1681
+ process.stderr.write(`gentle-shell: ${settingsPath} is not a JSON object, so Pi's package installs use the npm on your PATH\n`);
1682
+ } else if (edit.status === "write") {
1683
+ writeFileAtomically(settingsPath, edit.text);
1684
+ }
1685
+ }
1686
+
1687
+ // `gentle-shell self-uninstall` in a bundled install (S6, S7): the prefix's own
1688
+ // folders and exactly the PATH entry its receipt records; on Windows the running
1689
+ // node.exe and launcher are removed by a detached process after this one exits.
1690
+ async function handleBundledSelfUninstall(bundled, commandArgs) {
1691
+ const { askLine, runBundledSelfUninstall } = await import("../runtime/gentle-shell-uninstall.mjs");
1692
+ const { describePathReceipt, prefixLayout, readPathReceipt, removePathEntry, scheduleWindowsRemoval } = await import("../scripts/bundled-install.mjs");
1693
+ const windows = process.platform === "win32";
1694
+ const layout = prefixLayout({ platform: process.platform, env: process.env, home: homedir(), root: bundled.root });
1695
+ // Trusted only for the link or startup file the PATH plan can choose for this user.
1696
+ const receipt = readPathReceipt(layout, { env: process.env, home: homedir() });
1697
+ let pathEntry = receipt;
1698
+ if (receipt.status === "recorded") {
1699
+ const registry = windows ? (await import("../scripts/bundled-wizard.mjs")).windowsPathRegistry(process.env) : undefined;
1700
+ pathEntry = { status: "recorded", description: describePathReceipt(receipt.receipt), remove: () => removePathEntry(receipt.receipt, { registry }) };
1701
+ }
1702
+ return runBundledSelfUninstall({
1703
+ args: commandArgs,
1704
+ env: process.env,
1705
+ homedir: homedir(),
1706
+ platform: process.platform,
1707
+ interactive: process.stdin.isTTY === true,
1708
+ root: bundled.root,
1709
+ pathEntry,
1710
+ process: { pid: process.pid, execPath: process.execPath },
1711
+ ...(windows ? { deferRemoval: (request) => scheduleWindowsRemoval({ ...request, env: process.env }) } : {}),
1712
+ ask: (question) => askLine(question, { input: process.stdin, output: process.stdout }),
1713
+ out: (line) => process.stdout.write(`${line}\n`),
1714
+ err: (line) => process.stderr.write(`${line}\n`),
1715
+ });
1716
+ }
1717
+
1718
+ // `gentle-shell self-uninstall`: like upgrade, runs before any Pi runtime check,
1719
+ // so it also works when pi is missing.
1720
+ async function handleSelfUninstallCommand(commandArgs) {
1721
+ const { askLine, runSelfUninstall } = await import("../runtime/gentle-shell-uninstall.mjs");
1722
+ const { ownerManager } = await import("../scripts/main-channel.mjs");
1723
+ const managers = await packageManagerAdapters();
1724
+ const ctx = { env: process.env, home: homedir() };
1725
+ process.exitCode = await runSelfUninstall({
1726
+ args: commandArgs,
1727
+ env: process.env,
1728
+ homedir: homedir(),
1729
+ platform: process.platform,
1730
+ interactive: process.stdin.isTTY === true,
1731
+ resolveOwner: () => ownerManager({ ctx, platform: process.platform, packageRoot, adapters: managers, command: "uninstall" }),
1732
+ run: (command, argv) => managers.run(command, argv),
1733
+ ask: (question) => askLine(question, { input: process.stdin, output: process.stdout }),
1734
+ out: (line) => process.stdout.write(`${line}\n`),
1735
+ err: (line) => process.stderr.write(`${line}\n`),
1736
+ });
1737
+ }
1738
+
1739
+ // Before Pi exists: only the bundled interactive opening may offer an update.
1740
+ async function startupUpdate(bundled, args, startupEnv) {
1741
+ if (!shouldCheckStartupUpdate({ args, env: startupEnv, bundled: Boolean(bundled),
1742
+ inputTTY: process.stdin.isTTY === true, outputTTY: process.stdout.isTTY === true })) return undefined;
1743
+ const configPath = resolveConfigPath();
1744
+ let skipped;
1745
+ try { skipped = readRawConfig(configPath).skippedStartupRelease; } catch { /* A notice must not prevent opening. */ }
1746
+ const { latestRelease } = await import("../scripts/main-channel.mjs");
1747
+ const offer = await findStartupUpdate({ current: ownPackageVersion(), skipped,
1748
+ lookup: (signal) => latestRelease((url, options) => fetch(url, { ...options, signal })) });
1749
+ if (!offer) return undefined;
1750
+ let choice;
1751
+ try { choice = await promptStartupUpdate(offer, { color: process.stdout.hasColors?.() === true }); }
1752
+ catch { return undefined; }
1753
+ return applyStartupChoice(choice, {
1754
+ omit: () => omitStartupVersion(() => readJsonIfExists(configPath), (text) => {
1755
+ mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
1756
+ writeFileAtomically(configPath, text, { mode: 0o600 });
1757
+ }, offer.latest),
1758
+ warn: (message) => process.stderr.write(`gentle-shell: ${message}\n`),
1759
+ upgrade: () => handleBundledUpgrade(bundled, []),
1760
+ relaunch: async () => {
1761
+ const next = await resolveStartupRelaunch({ root: bundled.root, home: homedir(), platform: process.platform,
1762
+ env: process.env, argv: process.argv.slice(2) });
1763
+ return new Promise((resolve, reject) => {
1764
+ const child = spawn(next.command, next.args, { stdio: "inherit", env: next.env, shell: false });
1765
+ const forwards = ["SIGINT", "SIGTERM", "SIGHUP"].map((signal) => {
1766
+ const handler = () => child.kill(signal);
1767
+ process.on(signal, handler);
1768
+ return [signal, handler];
1769
+ });
1770
+ const cleanup = () => { for (const [signal, handler] of forwards) process.off(signal, handler); };
1771
+ child.once("error", (error) => { cleanup(); reject(error); });
1772
+ child.once("exit", (code, signal) => { cleanup(); resolve(signal ? signalExitCode(signal) : (code ?? 1)); });
1773
+ });
1774
+ },
1775
+ });
1776
+ }
1777
+
1046
1778
  async function main() {
1779
+ // One launch only: never leak the update handoff opt-out into Pi or its tools.
1780
+ const startupEnv = { ...process.env };
1781
+ delete process.env.GENTLE_SHELL_SKIP_STARTUP_UPDATE;
1047
1782
  const args = parseLauncherArgs(process.argv.slice(2));
1048
1783
  if (args.error !== undefined) fail(`${args.error}\nRun 'gentle-shell --help' for usage.`, 2);
1049
1784
  if (args.help) {
1050
- process.stdout.write(`${helpText()}\n`);
1785
+ 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. Back up data, update Engram, restart its server and Shell, then recheck.\nAn explicit ENGRAM_BIN or ENGRAM_URL is kept and bypasses the guard.\n`);
1051
1786
  process.exit(0);
1052
1787
  }
1053
1788
  if (args.command === "home") {
1054
1789
  handleHomeCommand(args.commandArgs);
1055
1790
  return;
1056
1791
  }
1792
+ // The verification input applies to `--version` only (scripts/bundled-install.mjs upgradeBundled).
1793
+ const verifying = args.version && args.command === undefined ? process.env[BUNDLED_VERIFY_ENV] : undefined;
1794
+ const bundled = resolveBundledInstall({ packageRoot, platform: process.platform, fs: bundledFs, verifying });
1795
+ if (args.command === "upgrade") {
1796
+ if (bundled) process.exitCode = await handleBundledUpgrade(bundled, args.commandArgs);
1797
+ else await handleUpgradeCommand(args.commandArgs);
1798
+ return;
1799
+ }
1800
+ if (args.command === "self-uninstall") {
1801
+ if (bundled) process.exitCode = await handleBundledSelfUninstall(bundled, args.commandArgs);
1802
+ else await handleSelfUninstallCommand(args.commandArgs);
1803
+ return;
1804
+ }
1805
+
1806
+ const startupCode = await startupUpdate(bundled, args, startupEnv);
1807
+ if (startupCode !== undefined) {
1808
+ process.exitCode = startupCode;
1809
+ return;
1810
+ }
1057
1811
 
1058
1812
  const config = loadConfig();
1059
1813
  let home = resolveHome({ args, env: process.env, homedir: homedir(), config });
1060
1814
  if (home.mode === "path") home = { ...home, dir: resolvePath(home.dir) };
1061
1815
 
1062
- const runtime = resolvePiRuntime({
1063
- env: process.env,
1064
- resolveBundledCli,
1065
- findOnPath,
1066
- nodeExecPath: process.execPath,
1067
- });
1068
- if (runtime === undefined) fail(missingPiMessage(), 1);
1816
+ // Version and help runs (Shell's own or forwarded to Pi) never probe Engram,
1817
+ // ensure the companion, print memory notices or touch provenance.
1818
+ const engramQuiet = args.version || piVersionOrHelpRequested(args.passthrough);
1819
+ let runtime;
1820
+ if (bundled) {
1821
+ // S8: only the version's own Pi, never GENTLE_SHELL_PI, an adjacent fallback or PATH.
1822
+ const pi = bundledPi(bundled, { platform: process.platform, fs: bundledFs });
1823
+ if (!pi.ok) fail(pi.message, 1);
1824
+ runtime = { kind: "bundled", command: process.execPath, args: [pi.cli] };
1825
+ bundledInstall = bundled;
1826
+ bundledCli = pi.cli;
1827
+ bundledMemory = engramQuiet ? { policy: "disabled", notice: ENGRAM_RECOVERY_NOTICE } : await classifyBundledMemory();
1828
+ bundledEnv = bundledProcessEnv();
1829
+ bundledSetupDirs = [dirname(process.execPath), join(bundled.versionDir, "node_modules", ".bin")];
1830
+ } else {
1831
+ runtime = resolvePiRuntime({
1832
+ env: process.env,
1833
+ resolveBundledCli,
1834
+ findOnPath,
1835
+ nodeExecPath: process.execPath,
1836
+ });
1837
+ if (runtime === undefined) fail(missingPiMessage(), 1);
1838
+ }
1069
1839
 
1070
1840
  const versionProbePlan = planSpawn({ command: runtime.command, args: [...runtime.args, "--version"], platform: process.platform });
1071
1841
  const versionProbe = spawnSync(versionProbePlan.command, versionProbePlan.args, {
@@ -1073,6 +1843,7 @@ async function main() {
1073
1843
  timeout: 15000,
1074
1844
  encoding: "utf8",
1075
1845
  shell: versionProbePlan.shell,
1846
+ env: bundledEnv ?? process.env,
1076
1847
  });
1077
1848
  if (versionProbe.error) fail(`Could not run the pi runtime at "${runtime.command}": ${versionProbe.error.message}`, 1);
1078
1849
  const versionCheck = checkPiVersion(versionProbe.stdout ?? "");
@@ -1083,6 +1854,23 @@ async function main() {
1083
1854
  process.exit(0);
1084
1855
  }
1085
1856
 
1857
+ // S5: in a bundled install Pi updates only with Gentle Shell, so Pi's own
1858
+ // update becomes `gentle-shell upgrade` (run when interactive, named
1859
+ // otherwise); package updates still reach Pi.
1860
+ let passthrough = args.passthrough;
1861
+ if (bundled && args.piSubcommand === "update") {
1862
+ const plan = bundledUpdatePlan(args.passthrough);
1863
+ if (plan.self) {
1864
+ const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
1865
+ for (const line of bundledUpdateNotice({ piVersion: versionCheck.version, interactive })) process.stdout.write(`${line}\n`);
1866
+ const code = interactive ? await handleBundledUpgrade(bundled, []) : undefined;
1867
+ const next = bundledUpdateNext(plan, code);
1868
+ if ("exitCode" in next) process.exit(next.exitCode);
1869
+ if (next.note) process.stderr.write(`${next.note}\n`);
1870
+ }
1871
+ passthrough = plan.piArgs;
1872
+ }
1873
+
1086
1874
  // Home-ownership signal for auto-provisioning (S9, homeIsForeign): must be
1087
1875
  // read before the isolated-home bootstrap below creates and seeds a
1088
1876
  // brand-new --home directory with its own settings.json — after that
@@ -1100,14 +1888,24 @@ async function main() {
1100
1888
  // Isolated-home bootstrap: only on a home gentle-shell has not seen before
1101
1889
  // (link never bootstraps — it reuses the user's own pi agent home as-is).
1102
1890
  if ((home.mode === "isolated" || home.mode === "path") && !existsSync(home.dir)) {
1103
- mkdirSync(home.dir, { recursive: true });
1891
+ // 0700: under umask 002 a default mode would leave ~/.gentle-shell group-writable,
1892
+ // which the bundled install's prefix claim must tighten or refuse.
1893
+ mkdirSync(home.dir, { recursive: true, mode: 0o700 });
1104
1894
  await installIsolatedTuiModeSetting(home.dir);
1105
1895
  writeHomeOwnershipMarker(home); // R3-001: lets a failed first auto-provision attempt still be retried later
1106
1896
  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`);
1107
1897
  }
1108
1898
 
1899
+ if (bundled) {
1900
+ try {
1901
+ await ensureBundledNpmCommand(bundled, home, homeHadContentBeforeBootstrap);
1902
+ } catch (error) {
1903
+ process.stderr.write(`gentle-shell: could not give Pi the bundled npm in ${join(home.dir, "settings.json")} (${error.message}); continuing\n`);
1904
+ }
1905
+ }
1906
+
1109
1907
  if (args.command === "setup") {
1110
- await handleSetupCommand(args.commandArgs, home, runtime);
1908
+ await handleSetupCommand(args.commandArgs, home, runtime, { homeHadContentBeforeBootstrap });
1111
1909
  return;
1112
1910
  }
1113
1911
 
@@ -1139,6 +1937,21 @@ async function main() {
1139
1937
  // exits with the same signal-derived code instead of falling through
1140
1938
  // to launch pi.
1141
1939
  if (autoProvisionResult !== undefined) process.exit(autoProvisionResult.exitCode);
1940
+
1941
+ // Runs after auto-provision, so it sees settings.json exactly as that
1942
+ // run left it. Only a home gentle-shell owns, by the same rule
1943
+ // auto-provisioning uses (homeIsForeign): never --link (excluded above),
1944
+ // a foreign --home, or pi's own default agent home, since plain pi may
1945
+ // share those and would lose its builtin codemode. `setup` (including
1946
+ // --dry-run) returned before this point.
1947
+ const settingsPath = join(home.dir, "settings.json");
1948
+ const excluded = safely("exclude Pi's builtin codemode in your Gentle Shell settings", settingsPath, false, () => {
1949
+ const previous = provisionedEntry(readRawConfig(resolveConfigPath()), safeRealpath(home.dir));
1950
+ return !homeIsForeign(home, previous, homeHadContentBeforeBootstrap) && ensureBuiltinCodemodeExcluded(settingsPath);
1951
+ });
1952
+ if (excluded) {
1953
+ process.stderr.write(`gentle-shell: disabled Pi's builtin codemode in ${settingsPath} (Gentle Shell ships its own codemode tool)\n`);
1954
+ }
1142
1955
  }
1143
1956
 
1144
1957
  const packageRootExplicit = args.packageRoot !== undefined;
@@ -1220,6 +2033,23 @@ async function main() {
1220
2033
  }
1221
2034
  }
1222
2035
 
2036
+ if (bundled) {
2037
+ // Recheck after setup and before provider loading: never carry a stale
2038
+ // fresh-store decision across an installer that could create data.
2039
+ if (args.piSubcommand === undefined && !engramQuiet) {
2040
+ bundledMemory = await classifyBundledMemory();
2041
+ bundledEnv = bundledProcessEnv();
2042
+ // The guard protects memory whatever provider loads, so a companion that
2043
+ // could not be verified is reported, never a reason to stop Shell.
2044
+ const companion = await ensureEngramCompanion(home, runtime, ["ignore", 2, 2], 10000);
2045
+ if (!companion.ok) process.stderr.write(`${companion.message}\n`);
2046
+ // A fresh store's identity and provenance exist before Pi starts, or
2047
+ // memory stays off: Pi never runs with allow without a written record.
2048
+ if (bundledMemory.fresh && !provisionEngramStore()) bundledMemory = { policy: "disabled", notice: ENGRAM_PROVISION_FAILED_NOTICE };
2049
+ if (bundledMemory.notice) process.stderr.write(`gentle-shell: ${bundledMemory.notice}\n`);
2050
+ }
2051
+ bundledEnv = bundledProcessEnv();
2052
+ }
1223
2053
  const invocation = buildPiInvocation({
1224
2054
  runtime,
1225
2055
  home,
@@ -1228,22 +2058,98 @@ async function main() {
1228
2058
  takeOver,
1229
2059
  otherPackagePaths,
1230
2060
  looseExtensionEntries,
1231
- passthrough: args.passthrough,
2061
+ passthrough: [...managedHerdrExtensionArgs(home, args), ...passthrough],
1232
2062
  piSubcommand: args.piSubcommand,
1233
- baseEnv: process.env,
2063
+ baseEnv: bundledEnv ?? process.env,
2064
+ homedir: homedir(),
2065
+ // The spawn below sets no cwd, so pi runs in the launcher's own.
2066
+ cwd: process.cwd(),
1234
2067
  });
1235
2068
 
2069
+ // Only an interactive session ends with pi's exit resume hint, which
2070
+ // gentle-shell completes with its own line; a pi subcommand gets no handoff.
2071
+ const resumeHandoff = args.piSubcommand === undefined ? createResumeHandoff() : undefined;
2072
+ const childEnv = resumeHandoff ? { ...invocation.env, [RESUME_HANDOFF_ENV]: resumeHandoff.path } : invocation.env;
2073
+
1236
2074
  const launchPlan = planSpawn({ command: invocation.command, args: invocation.args, platform: process.platform });
1237
- const child = spawn(launchPlan.command, launchPlan.args, { stdio: "inherit", env: invocation.env, shell: launchPlan.shell });
2075
+ let child;
2076
+ try {
2077
+ child = spawn(launchPlan.command, launchPlan.args, { stdio: "inherit", env: childEnv, shell: launchPlan.shell });
2078
+ } catch (error) {
2079
+ resumeHandoff?.dispose();
2080
+ throw error;
2081
+ }
2082
+ // Only SIGHUP means the terminal is gone. pi may survive a forwarded
2083
+ // SIGINT and keep running, so other signals must not silence the hint.
2084
+ let terminalHungUp = false;
1238
2085
  for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
1239
- process.on(signal, () => child.kill(signal));
2086
+ process.on(signal, () => {
2087
+ if (signal === "SIGHUP") terminalHungUp = true;
2088
+ child.kill(signal);
2089
+ });
1240
2090
  }
1241
- child.on("error", (error) => fail(`Could not start pi: ${error.message}`, 1));
2091
+ child.on("error", (error) => {
2092
+ resumeHandoff?.dispose();
2093
+ fail(`Could not start pi: ${error.message}`, 1);
2094
+ });
1242
2095
  child.on("exit", (code, signal) => {
1243
- process.exit(signal ? signalExitCode(signal) : (code ?? 1));
2096
+ const exitCode = signal ? signalExitCode(signal) : (code ?? 1);
2097
+ if (resumeHandoff) {
2098
+ const hint = planResumeHint({
2099
+ handoff: resumeHandoff.read(),
2100
+ homeFlags: homeSelectorFlags(home),
2101
+ stdoutIsTTY: process.stdout.isTTY === true,
2102
+ terminalHungUp,
2103
+ platform: process.platform,
2104
+ color: process.stdout.hasColors?.() === true,
2105
+ });
2106
+ resumeHandoff.dispose();
2107
+ // TTY writes are asynchronous on Windows: exit only once the
2108
+ // hint is flushed, or it can be lost.
2109
+ if (hint) {
2110
+ // A write error (e.g. EIO on a closed terminal) must not turn
2111
+ // pi's exit into a launcher crash.
2112
+ process.stdout.once("error", () => process.exit(exitCode));
2113
+ process.stdout.write(hint, () => process.exit(exitCode));
2114
+ return;
2115
+ }
2116
+ }
2117
+ process.exit(exitCode);
1244
2118
  });
1245
2119
  }
1246
2120
 
2121
+ // Private temp dir for the resume-hint handoff (lib/gentle-shell-resume-hint.ts).
2122
+ // Best effort: if it cannot be created, only pi's own hint is printed.
2123
+ function createResumeHandoff() {
2124
+ let dir;
2125
+ try {
2126
+ dir = mkdtempSync(join(tmpdir(), RESUME_HANDOFF_DIR_PREFIX));
2127
+ } catch {
2128
+ return undefined;
2129
+ }
2130
+ const path = join(dir, RESUME_HANDOFF_FILE);
2131
+ return {
2132
+ path,
2133
+ read: () => {
2134
+ try {
2135
+ const text = readJsonIfExists(path);
2136
+ return text === undefined ? undefined : parseResumeHandoff(text);
2137
+ } catch {
2138
+ return undefined;
2139
+ }
2140
+ },
2141
+ // Never throws: it runs inside the exit handler, where an EPERM on
2142
+ // Windows would otherwise replace pi's exit code with a crash.
2143
+ dispose: () => {
2144
+ try {
2145
+ rmSync(dir, { recursive: true, force: true });
2146
+ } catch {
2147
+ // A leftover empty temp dir is harmless.
2148
+ }
2149
+ },
2150
+ };
2151
+ }
2152
+
1247
2153
  main().catch((error) => {
1248
2154
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
1249
2155
  process.exit(1);