gentle-pi 4.2.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (334) hide show
  1. package/README.md +340 -138
  2. package/assets/agents/gentle-ai-explore.md +1 -0
  3. package/assets/agents/gentle-ai-verify.md +6 -4
  4. package/assets/agents/gentle-ai-worker.md +7 -1
  5. package/assets/design-canvas/canvas.css +648 -0
  6. package/assets/design-canvas/canvas.js +1497 -0
  7. package/assets/design-canvas/index.html +88 -0
  8. package/assets/design-canvas/inspector.js +290 -0
  9. package/assets/orchestrator-delegation-reference.md +163 -0
  10. package/assets/orchestrator-delegation.md +25 -176
  11. package/assets/orchestrator-tracking.md +1 -1
  12. package/assets/orchestrator-verification.md +14 -8
  13. package/assets/orchestrator-writer.md +5 -1
  14. package/assets/orchestrator.md +11 -11
  15. package/bin/engram-guard +1 -1
  16. package/bin/gentle-shell.mjs +237 -139
  17. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  18. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
  19. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
  20. package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
  21. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
  22. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
  23. package/docs/assets/diagrams/agent-orchestration.svg +54 -15
  24. package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
  25. package/docs/assets/diagrams/measured-routing.svg +49 -0
  26. package/docs/assets/diagrams/native-review.svg +52 -12
  27. package/docs/assets/diagrams/odd-workflow.svg +128 -66
  28. package/docs/assets/diagrams/permission-modes.svg +51 -0
  29. package/docs/assets/features/audio-notifications.png +0 -0
  30. package/docs/assets/features/command-palette.png +0 -0
  31. package/docs/bundled-install.md +65 -30
  32. package/docs/delegated-verification.md +16 -8
  33. package/docs/gentle-agents-activity.md +16 -6
  34. package/docs/gentle-music.md +61 -0
  35. package/docs/gentle-shell-uninstall.md +8 -1
  36. package/docs/gentle-shell.md +80 -9
  37. package/docs/install-wizard.md +37 -6
  38. package/docs/permission-modes.md +8 -3
  39. package/docs/readme-reference.md +120 -28
  40. package/docs/session-profile-format.md +26 -0
  41. package/docs/voice.md +259 -0
  42. package/docs/yolo-mode.md +38 -10
  43. package/extensions/ask-user-choice.ts +4 -1
  44. package/extensions/codegraph-tools.ts +68 -6
  45. package/extensions/gentle-agents.ts +270 -48
  46. package/extensions/gentle-ai.ts +490 -113
  47. package/extensions/gentle-design.ts +158 -0
  48. package/extensions/gentle-herdr-activity.ts +2 -3
  49. package/extensions/gentle-music.ts +289 -0
  50. package/extensions/gentle-shell.ts +359 -85
  51. package/extensions/gentle-todo.ts +43 -9
  52. package/extensions/gentle-voice.ts +11 -0
  53. package/extensions/quiet-tools.ts +1 -1
  54. package/extensions/session-scratch.ts +384 -0
  55. package/extensions/skill-registry.ts +63 -8
  56. package/extensions/startup-banner.ts +2 -3
  57. package/lib/agent-home.ts +8 -3
  58. package/lib/agent-profiles.ts +26 -0
  59. package/lib/agents-config.ts +35 -0
  60. package/lib/agents-protocol.ts +60 -1
  61. package/lib/agents-runner.ts +107 -9
  62. package/lib/agents-session-transport.ts +101 -6
  63. package/lib/agents-view.ts +3 -1
  64. package/lib/agents-widget.ts +14 -1
  65. package/lib/auto-permission-memory.ts +48 -18
  66. package/lib/auto-permission-runtime.ts +21 -6
  67. package/lib/bounded-writer-admission.ts +51 -0
  68. package/lib/clipboard-image.ts +147 -0
  69. package/lib/command-palette-catalog.ts +2 -4
  70. package/lib/commit-slice-signal.ts +100 -0
  71. package/lib/context-backstop.ts +120 -0
  72. package/lib/delivered-test-lint.ts +80 -0
  73. package/lib/design-canvas-server.ts +947 -0
  74. package/lib/design-comments.ts +437 -0
  75. package/lib/design-handoffs.ts +313 -0
  76. package/lib/design-open-browser.ts +58 -0
  77. package/lib/design-sources.ts +120 -0
  78. package/lib/destructive-command-guard.ts +120 -9
  79. package/lib/external-editor.ts +31 -0
  80. package/lib/gauge-theme.ts +146 -0
  81. package/lib/gentle-ai-binary.ts +28 -7
  82. package/lib/gentle-design.ts +513 -0
  83. package/lib/gentle-shell-bundled.ts +146 -36
  84. package/lib/gentle-shell-import-pi.ts +233 -0
  85. package/lib/gentle-shell-launcher.ts +154 -15
  86. package/lib/gentle-shell-startup-update.ts +61 -9
  87. package/lib/gentle-shell-uninstall.ts +51 -2
  88. package/lib/herdr-activity.ts +24 -0
  89. package/lib/image-attachments.ts +150 -0
  90. package/lib/inprocess-reviewer.ts +33 -2
  91. package/lib/nan-provider.ts +3 -0
  92. package/lib/native-review-cli.ts +20 -5
  93. package/lib/notification-audio-native.ts +149 -13
  94. package/lib/notification-audio.ts +4 -0
  95. package/lib/notification-pulse-worker.ts +82 -18
  96. package/lib/notification-service.ts +5 -1
  97. package/lib/orchestrator-discovery.ts +5 -1
  98. package/lib/orchestrator-presence.ts +87 -8
  99. package/lib/orchestrator-scope.ts +31 -3
  100. package/lib/orchestrator-state.ts +11 -3
  101. package/lib/permission-mode-policy.ts +2 -2
  102. package/lib/provider-contract-bundle.ts +99 -53
  103. package/lib/review-candidate-view-owner.ts +29 -19
  104. package/lib/review-candidate-view.ts +36 -2
  105. package/lib/review-host-relay.ts +11 -2
  106. package/lib/review-reminder-receipt.ts +24 -1
  107. package/lib/review-risk-assessment.ts +43 -38
  108. package/lib/review-session-standing-permission.ts +3 -1
  109. package/lib/session-profile-append-controller.ts +713 -0
  110. package/lib/session-profile-disk-reader.ts +182 -0
  111. package/lib/session-profile-freeze.ts +30 -1
  112. package/lib/session-profile-persistence.ts +15 -7
  113. package/lib/session-scratch.ts +523 -0
  114. package/lib/session-worktree-registry.ts +7 -0
  115. package/lib/shell-bar.ts +29 -28
  116. package/lib/shell-changes-view.ts +80 -13
  117. package/lib/shell-changes.ts +81 -5
  118. package/lib/shell-gauge.ts +49 -6
  119. package/lib/shell-prompt.ts +4 -4
  120. package/lib/shell-sidebar-layout.ts +26 -6
  121. package/lib/shell-todo.ts +24 -5
  122. package/lib/shell-usage-view.ts +5 -1
  123. package/lib/shell-usage.ts +7 -7
  124. package/lib/verifier-admission.ts +30 -0
  125. package/lib/vim-editor-adapter.ts +11 -8
  126. package/lib/voice-artifacts.ts +188 -0
  127. package/lib/voice-command.ts +576 -0
  128. package/lib/voice-install.ts +455 -0
  129. package/lib/voice-platform.ts +99 -0
  130. package/lib/voice-push-to-talk.ts +262 -0
  131. package/lib/voice-session.ts +460 -0
  132. package/lib/voice-settings.ts +50 -0
  133. package/lib/voice-ui.ts +158 -0
  134. package/lib/yolo-session-policy.ts +58 -22
  135. package/lib/yolo-temp-scratch.ts +69 -0
  136. package/package.json +1 -2
  137. package/runtime/gentle-ai-binary.mjs +28 -7
  138. package/runtime/gentle-shell-bundled.mjs +145 -35
  139. package/runtime/gentle-shell-import-pi.mjs +234 -0
  140. package/runtime/gentle-shell-launcher.mjs +152 -13
  141. package/runtime/gentle-shell-startup-update.mjs +61 -9
  142. package/runtime/gentle-shell-uninstall.mjs +51 -2
  143. package/runtime/native-review-cli.mjs +19 -4
  144. package/runtime/review-risk-assessment.mjs +43 -38
  145. package/scripts/build-runtime-modules.mjs +1 -0
  146. package/scripts/bundled-engram-migration.mjs +119 -0
  147. package/scripts/gentle-ai-installer.mjs +10 -10
  148. package/scripts/installer-downloads.mjs +7 -7
  149. package/scripts/installer-preflight.mjs +5 -3
  150. package/scripts/installer-server.mjs +14 -4
  151. package/scripts/installer-windows.mjs +37 -7
  152. package/scripts/main-channel.mjs +94 -19
  153. package/scripts/pi-update.mjs +34 -6
  154. package/scripts/run-test-suite.mjs +5 -0
  155. package/scripts/verify-package-files.mjs +43 -13
  156. package/scripts/voice-worker.cjs +476 -0
  157. package/skills/design/SKILL.md +110 -0
  158. package/skills/gentle-ai/SKILL.md +3 -3
  159. package/skills/work-unit-commits/SKILL.md +5 -3
  160. package/tests/agent-profiles.test.ts +24 -0
  161. package/tests/agent-risk-escalation.test.ts +37 -10
  162. package/tests/agents-config.test.ts +61 -0
  163. package/tests/agents-fake-child.ts +8 -1
  164. package/tests/agents-protocol.test.ts +32 -0
  165. package/tests/agents-runner.test.ts +485 -7
  166. package/tests/agents-session-transport.test.ts +53 -3
  167. package/tests/ask-user-choice.test.ts +30 -0
  168. package/tests/auto-mode.test.ts +17 -0
  169. package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
  170. package/tests/auto-permission-runtime.test.ts +85 -1
  171. package/tests/autonomous-guard.test.ts +142 -0
  172. package/tests/bounded-writer-admission.test.ts +11 -3
  173. package/tests/bundled-install.test.ts +3 -3
  174. package/tests/card-content-policy.test.ts +19 -9
  175. package/tests/child-safety.test.ts +39 -0
  176. package/tests/clipboard-image.test.ts +114 -0
  177. package/tests/codegraph-tools.test.ts +123 -2
  178. package/tests/command-palette.test.ts +15 -5
  179. package/tests/commit-slice-signal.test.ts +162 -0
  180. package/tests/context-backstop.test.ts +225 -0
  181. package/tests/delegate-for-reason-contract.test.ts +92 -24
  182. package/tests/delivered-test-lint.test.ts +105 -0
  183. package/tests/design-canvas-assets.test.ts +710 -0
  184. package/tests/design-canvas-server.test.ts +1096 -0
  185. package/tests/design-comments.test.ts +201 -0
  186. package/tests/design-handoffs.test.ts +282 -0
  187. package/tests/design-open-browser.test.ts +70 -0
  188. package/tests/design-sources.test.ts +132 -0
  189. package/tests/destructive-command-guard.test.ts +85 -1
  190. package/tests/engram-safety.test.ts +132 -57
  191. package/tests/external-editor.test.ts +84 -0
  192. package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
  193. package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
  194. package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
  195. package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
  196. package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
  197. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
  198. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
  199. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
  200. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
  201. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
  202. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
  203. package/tests/gauge-theme.test.ts +120 -0
  204. package/tests/gentle-agents.test.ts +645 -21
  205. package/tests/gentle-ai-binary.test.ts +1 -1
  206. package/tests/gentle-ai-child-guards.test.ts +61 -0
  207. package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
  208. package/tests/gentle-ai-installer.test.ts +51 -51
  209. package/tests/gentle-ai.test.ts +292 -13
  210. package/tests/gentle-design.test.ts +1313 -0
  211. package/tests/gentle-music.test.ts +335 -0
  212. package/tests/gentle-shell-bin.test.ts +163 -0
  213. package/tests/gentle-shell-bundled.test.ts +392 -176
  214. package/tests/gentle-shell-import-pi.test.ts +262 -0
  215. package/tests/gentle-shell-launcher.test.ts +107 -2
  216. package/tests/gentle-shell-startup-update.test.ts +77 -1
  217. package/tests/gentle-shell-uninstall.test.ts +89 -0
  218. package/tests/gentle-shell.test.ts +225 -17
  219. package/tests/gentle-todo.test.ts +190 -0
  220. package/tests/headless-delivery-question.test.ts +58 -0
  221. package/tests/herdr-activity.test.ts +34 -1
  222. package/tests/image-attachments.test.ts +119 -0
  223. package/tests/image-paste-chips.test.ts +236 -0
  224. package/tests/inprocess-reviewer.test.ts +55 -2
  225. package/tests/installer-bundles.test.ts +12 -6
  226. package/tests/installer-posix-bootstrap.test.ts +8 -8
  227. package/tests/installer-preflight.test.ts +16 -0
  228. package/tests/installer-server.test.ts +10 -2
  229. package/tests/installer-windows-bootstrap.test.ts +94 -7
  230. package/tests/installer-windows-diagnostics.test.ts +111 -0
  231. package/tests/main-channel-upgrade.test.ts +232 -2
  232. package/tests/main-channel.test.ts +140 -21
  233. package/tests/nan-provider.test.ts +36 -0
  234. package/tests/native-review-capability-contract.test.ts +17 -4
  235. package/tests/native-review-parity.test.ts +23 -2
  236. package/tests/no-self-pause-contract.test.ts +5 -2
  237. package/tests/notification-audio-native.test.ts +128 -9
  238. package/tests/notification-customize.test.ts +17 -5
  239. package/tests/odd-integration.test.ts +5 -1
  240. package/tests/odd-routing-canonical-ratchet.test.ts +128 -162
  241. package/tests/odd-routing-contract.test.ts +115 -30
  242. package/tests/orchestrator-budget.test.ts +22 -8
  243. package/tests/orchestrator-discovery.test.ts +29 -0
  244. package/tests/orchestrator-presence.test.ts +100 -2
  245. package/tests/orchestrator-rdd-ownership.test.ts +3 -1
  246. package/tests/orchestrator-scope.test.ts +80 -0
  247. package/tests/orchestrator-state.test.ts +29 -0
  248. package/tests/orchestrator-status-publication-contract.test.ts +12 -8
  249. package/tests/package-files.test.ts +34 -0
  250. package/tests/package-manifest.test.ts +24 -20
  251. package/tests/permission-mode-policy.test.ts +10 -1
  252. package/tests/pi-update-launcher.test.ts +21 -2
  253. package/tests/pi-update.test.ts +99 -4
  254. package/tests/primary-session-prompt.test.ts +101 -0
  255. package/tests/provider-contract-bundle.test.ts +118 -0
  256. package/tests/provider-contract-mirror.test.ts +24 -0
  257. package/tests/quiet-tool-rendering.test.ts +23 -0
  258. package/tests/rdd-aware-verification-contract.test.ts +29 -11
  259. package/tests/rdd-status-line.test.ts +79 -0
  260. package/tests/review-agent-end-preflight.test.ts +6 -0
  261. package/tests/review-candidate-view.test.ts +49 -0
  262. package/tests/review-contract-prompt.test.ts +5 -2
  263. package/tests/review-controller-native-routing.test.ts +14 -1
  264. package/tests/review-controller.test.ts +1 -1
  265. package/tests/review-host-relay-routing.test.ts +147 -3
  266. package/tests/review-host-relay.test.ts +9 -2
  267. package/tests/review-json-arguments.test.ts +20 -3
  268. package/tests/review-loadout-rdd-off-sdk.test.ts +258 -0
  269. package/tests/review-owner-identity.test.ts +132 -0
  270. package/tests/review-relay-transport-agent.test.ts +26 -0
  271. package/tests/review-reminder-receipt.test.ts +31 -1
  272. package/tests/review-risk-assessment.test.ts +163 -76
  273. package/tests/review-session-standing-permission-controller.test.ts +14 -1
  274. package/tests/review-session-standing-permission.test.ts +27 -1
  275. package/tests/run-test-suite.test.ts +15 -0
  276. package/tests/runtime-harness.mjs +16 -6
  277. package/tests/session-changes-shell.test.ts +1 -1
  278. package/tests/session-changes.test.ts +2 -1
  279. package/tests/session-profile-append-controller-fork.test.ts +458 -0
  280. package/tests/session-profile-append-controller-pi.test.ts +290 -0
  281. package/tests/session-profile-append-controller.test.ts +731 -0
  282. package/tests/session-profile-disk-reader-pi.test.ts +183 -0
  283. package/tests/session-profile-disk-reader.test.ts +311 -0
  284. package/tests/session-profile-freeze.test.ts +23 -0
  285. package/tests/session-scratch.test.ts +1011 -0
  286. package/tests/session-subject-routing-contract.test.ts +2 -1
  287. package/tests/session-worktree-registry.test.ts +13 -1
  288. package/tests/shell-bar.test.ts +21 -2
  289. package/tests/shell-changes-view.test.ts +137 -1
  290. package/tests/shell-changes.test.ts +148 -1
  291. package/tests/shell-gauge.test.ts +42 -0
  292. package/tests/shell-prompt.test.ts +3 -3
  293. package/tests/shell-render-cache.test.ts +134 -1
  294. package/tests/shell-sidebar-layout.test.ts +50 -0
  295. package/tests/short-check-routing-contract.test.ts +1 -1
  296. package/tests/skill-collision-prefixes.test.ts +3 -1
  297. package/tests/skill-registry-watcher.test.ts +17 -5
  298. package/tests/skill-registry.test.ts +47 -3
  299. package/tests/small-task-inline-contract.test.ts +37 -0
  300. package/tests/standalone-config-home.test.ts +324 -0
  301. package/tests/startup-banner.test.ts +5 -4
  302. package/tests/startup-home-repo.test.ts +34 -0
  303. package/tests/support/orchestrator-modules.ts +1 -0
  304. package/tests/task-size-routing-contract.test.ts +70 -10
  305. package/tests/test-scratch-contract.test.ts +102 -0
  306. package/tests/test-strength-contract.test.ts +36 -1
  307. package/tests/tool-batching-contract.test.ts +64 -0
  308. package/tests/verification-proportionality.test.ts +46 -0
  309. package/tests/verifier-admission.test.ts +154 -0
  310. package/tests/verify-agent-contract.test.ts +8 -8
  311. package/tests/voice-command.test.ts +773 -0
  312. package/tests/voice-install.test.ts +323 -0
  313. package/tests/voice-platform.test.ts +176 -0
  314. package/tests/voice-push-to-talk.test.ts +444 -0
  315. package/tests/voice-session.test.ts +434 -0
  316. package/tests/voice-settings.test.ts +49 -0
  317. package/tests/voice-ui.test.ts +97 -0
  318. package/tests/voice-worker.test.ts +445 -0
  319. package/tests/weak-model-economy-prompt.test.ts +32 -0
  320. package/tests/windows-hidden-processes.test.ts +30 -2
  321. package/tests/writer-edit-surface-scope.test.ts +3 -0
  322. package/tests/writer-feature-document-admission.test.ts +156 -0
  323. package/tests/yolo-customize.test.ts +5 -1
  324. package/tests/yolo-mode.test.ts +19 -1
  325. package/tests/yolo-session-policy-capture.test.ts +104 -0
  326. package/tests/yolo-temp-scratch.test.ts +71 -0
  327. package/scripts/mirror-odd-routing.mjs +0 -242
  328. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
  329. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
  330. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
  331. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  332. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
  333. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
  334. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -9,6 +9,7 @@ import {
9
9
  chmodSync,
10
10
  closeSync,
11
11
  constants as fsConstants,
12
+ copyFileSync,
12
13
  existsSync,
13
14
  fstatSync,
14
15
  lstatSync,
@@ -27,7 +28,6 @@ import {
27
28
  import { constants as osConstants, homedir, tmpdir } from "node:os";
28
29
  import { delimiter, dirname, join, resolve as resolvePath } from "node:path";
29
30
  import { spawn, spawnSync } from "node:child_process";
30
- import { createHash } from "node:crypto";
31
31
  import { fileURLToPath } from "node:url";
32
32
  import {
33
33
  buildPiInvocation,
@@ -50,26 +50,30 @@ import {
50
50
  parseRawLauncherConfig,
51
51
  planSpawn,
52
52
  POST_INSTALL_REMOVAL_SOURCES,
53
+ withLauncherPackagesExcluded,
53
54
  postInstallRemovals,
54
55
  provisionedEntry,
56
+ migrateStandaloneConfig,
55
57
  recordProvisioned,
56
58
  resolveHome,
57
59
  resolvePiRuntime,
58
60
  restoreJsonField,
61
+ separateHomeHint,
59
62
  shellQuote,
63
+ standaloneConfigHome,
64
+ userPiHome,
60
65
  } from "../runtime/gentle-shell-launcher.mjs";
66
+ import { formatImportPiReport, importPi } from "../runtime/gentle-shell-import-pi.mjs";
61
67
  import {
62
68
  BUNDLED_VERIFY_ENV,
63
- bundledEngramSchema,
64
69
  classifyEngramStartup,
65
70
  ENGRAM_COMPANION_SOURCE,
66
- ENGRAM_PROVISION_FAILED_NOTICE,
71
+ ENGRAM_DEFAULT_URL,
67
72
  ENGRAM_RECOVERY_NOTICE,
68
- engramHeaderAllows,
73
+ ENGRAM_START_FAILED_NOTICE,
74
+ GENTLE_SHELL_CONFIG_HOME_ENV,
69
75
  engramProbeAllowed,
70
- engramProvenanceMatches,
71
- engramProvenancePath,
72
- parseEngramProvenance,
76
+ sharedEngramEnv,
73
77
  piVersionOrHelpRequested,
74
78
  probeEngramEndpoint,
75
79
  sqliteHeaderUserVersion,
@@ -84,6 +88,7 @@ import {
84
88
  parseBundledUpgradeArgs,
85
89
  resolveBundledInstall,
86
90
  withBundledNpmCommand,
91
+ withoutMissingBundledNpmCommand,
87
92
  } from "../runtime/gentle-shell-bundled.mjs";
88
93
  import {
89
94
  parseResumeHandoff,
@@ -95,7 +100,8 @@ import {
95
100
  import { bundledGentleAiPaths, GENTLE_AI_VERSION, gentleAiBinaryPath, gentleAiIsolationEnv, PackageLocalGentleAiBinaryMissingError } from "../runtime/gentle-ai-binary.mjs";
96
101
  import { DEFAULT_THEME_NAME, installIsolatedTuiModeSetting } from "../scripts/install-tui-mode-setting.mjs";
97
102
  import { artifactFor, engramPinVersion } from "../scripts/installer-downloads.mjs";
98
- import { shouldCheckStartupUpdate, findStartupUpdate } from "../runtime/gentle-shell-startup-update.mjs";
103
+ import { migrateBundledEngram, migrationPending, MIGRATION_PENDING_NOTICE, MIGRATION_USAGE } from "../scripts/bundled-engram-migration.mjs";
104
+ import { findStartupUpdate, parseStartupReleaseCache, shouldCheckStartupUpdate, writeStartupReleaseCache } from "../runtime/gentle-shell-startup-update.mjs";
99
105
  import { promptStartupUpdate } from "../runtime/gentle-shell-update-panel.mjs";
100
106
  import { applyStartupChoice, omitStartupVersion, resolveStartupRelaunch } from "../runtime/gentle-shell-startup-flow.mjs";
101
107
 
@@ -192,19 +198,6 @@ function verifiedBundledCore() {
192
198
  return undefined;
193
199
  }
194
200
 
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
201
  // The database's raw SQLite header, read as a plain file (never through
209
202
  // SQLite): a regular, unlinked engram.db whose opened identity is the one seen.
210
203
  function readSqliteUserVersion(path) {
@@ -224,100 +217,49 @@ function readSqliteUserVersion(path) {
224
217
  }
225
218
  }
226
219
 
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.
220
+ // Evidence only: the default store's raw header and a bounded read-only probe
221
+ // of the default endpoint. No Engram executable, SQLite, or contact with an
222
+ // explicitly selected endpoint. A `spawn` decision starts nothing here.
270
223
  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();
224
+ if (migrationPending(bundledInstall.root)) return { policy: "disabled", notice: MIGRATION_PENDING_NOTICE };
225
+ const decision = { env: process.env, core: verifiedBundledCore() !== undefined, coreVersion: engramPinVersion };
226
+ if (!engramProbeAllowed(process.env)) return classifyEngramStartup({ ...decision, endpoint: { kind: "unknown" } });
275
227
  const endpoint = await probeEngramEndpoint();
276
- return classifyEngramStartup({ ...decision, store: observed.store, endpoint, provenance: ownEngramStore(observed, core) });
277
- }
278
-
279
- // Verified with the published Engram v3.4.0 darwin-arm64 binary in an isolated
280
- // HOME: `instance-id` creates only `.instance-id.lock` and `.instance-id`,
281
- // without a database. Keep update checks suppressed and GitHub tokens removed
282
- // for this provisioning invocation. The timeout stays below the identity lock
283
- // wait: a held lock means a concurrent creator, so memory stays off that launch.
284
- const ENGRAM_PROVISION_TIMEOUT_MS = 15000;
285
-
286
- // A fresh launch's store identity, provisioned by Shell itself with the verified
287
- // core before Pi starts, then recorded. False when any step fails.
288
- function provisionEngramStore() {
228
+ const userVersion = endpoint.kind === "refused" ? readSqliteUserVersion(join(homedir(), ".engram", "engram.db")) : undefined;
229
+ return classifyEngramStartup({ ...decision, endpoint, userVersion });
230
+ }
231
+
232
+ // Like other agents' hooks, Shell starts the shared server when nothing
233
+ // listens: the verified core's plain `serve` on Engram's own default port and
234
+ // data directory, detached so it outlives Pi. Its environment drops the user's
235
+ // Engram settings and GitHub tokens and skips the update check. Ready means the
236
+ // same /compatibility contract any reused server must answer, so a concurrent
237
+ // launch that wins the bind converges too: after the child exits, polling
238
+ // continues for a short grace within the overall bound.
239
+ const ENGRAM_START_TIMEOUT_MS = 10000;
240
+ const ENGRAM_START_EXIT_GRACE_MS = 2000;
241
+
242
+ async function startSharedEngram() {
243
+ const failed = { policy: "disabled", notice: ENGRAM_START_FAILED_NOTICE };
289
244
  const core = verifiedBundledCore();
290
- if (core === undefined || observeEngramStore().store !== "absent") return false;
291
- let target;
245
+ if (core === undefined) return failed;
246
+ let deadline = Date.now() + ENGRAM_START_TIMEOUT_MS;
247
+ const exited = () => { deadline = Math.min(deadline, Date.now() + ENGRAM_START_EXIT_GRACE_MS); };
292
248
  try {
293
- target = join(realpathSync(homedir()), ".engram");
249
+ const env = sharedEngramEnv(process.env);
250
+ const child = spawn(core, ["serve"], { cwd: homedir(), env, detached: true, stdio: "ignore", windowsHide: true });
251
+ child.once("error", exited);
252
+ child.once("exit", exited);
253
+ child.unref();
294
254
  } catch {
295
- return false;
255
+ return failed;
296
256
  }
297
- // The provider's own environment, except for this one invocation: no update
298
- // check and no GitHub tokens. ENGRAM_DATA_DIR is never set here.
299
- const env = { ...process.env, ENGRAM_NO_UPDATE_CHECK: "1" };
300
- delete env.GH_TOKEN;
301
- delete env.GITHUB_TOKEN;
302
- const result = spawnSync(core, ["instance-id"], { env, encoding: "utf8", timeout: ENGRAM_PROVISION_TIMEOUT_MS, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
303
- const observed = observeEngramStore();
304
- if (result.status !== 0 || observed.store !== "present" || observed.dataDir !== target || !/^[a-f0-9]{32}$/.test(observed.instanceId ?? "") ||
305
- String(result.stdout).trim() !== observed.instanceId) return false;
306
- return writeEngramProvenance({ schema: 1, dataDir: target, instanceId: observed.instanceId, coreSha256: sha256File(core) });
307
- }
308
-
309
- function writeEngramProvenance(record) {
310
- try {
311
- const path = engramProvenancePath(bundledInstall.root, process.platform);
312
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
313
- const temporary = `${path}.${process.pid}.tmp`;
314
- writeFileSync(temporary, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600, flag: "wx" });
315
- renameSync(temporary, path);
316
- return true;
317
- } catch (error) {
318
- process.stderr.write(`gentle-shell: could not record Engram provenance (${error.message})\n`);
319
- return false;
257
+ while (Date.now() < deadline) {
258
+ const endpoint = await probeEngramEndpoint({ timeoutMs: 500 });
259
+ if (endpoint.kind === "listening" && endpoint.compatible) return { policy: "allow", url: ENGRAM_DEFAULT_URL };
260
+ await new Promise((resolve) => setTimeout(resolve, 100));
320
261
  }
262
+ return failed;
321
263
  }
322
264
 
323
265
  // Use Pi's supported configuration-only install command, never an alternate
@@ -627,6 +569,42 @@ function handleHomeCommand(commandArgs) {
627
569
  process.exit(0);
628
570
  }
629
571
 
572
+ // `gentle-shell [home selectors] home import-pi [--dry-run]`: copies the
573
+ // user's pi sign-ins, MCP servers and default model into the resolved
574
+ // separate home (lib/gentle-shell-import-pi.ts). A home that is new or empty
575
+ // is first bootstrapped exactly as a first launch would, so the files this
576
+ // adds never make it look foreign to auto-provisioning (homeIsForeign).
577
+ async function handleImportPiCommand(args) {
578
+ const options = args.commandArgs.slice(1);
579
+ if (options.some((option) => option !== "--dry-run")) fail("gentle-shell home import-pi accepts only --dry-run. Run 'gentle-shell --help'.", 2);
580
+ const dryRun = options.includes("--dry-run");
581
+ let home = resolveHome({ args, env: process.env, homedir: homedir(), config: loadConfig() });
582
+ if (home.mode === "path") home = { ...home, dir: resolvePath(home.dir) };
583
+ const source = resolvePath(userPiHome(process.env, homedir()));
584
+ const refusal = "Choose a separate home with --isolated or --home <path>.";
585
+ if (home.mode === "link") {
586
+ fail(`gentle-shell home import-pi: the selected home is your pi home itself (--link), so there is nothing to import. ${refusal}`, 1);
587
+ }
588
+ if ([source, join(homedir(), ".pi", "agent")].some((piHome) => safeRealpath(piHome) === safeRealpath(home.dir))) {
589
+ fail(`gentle-shell home import-pi: ${home.dir} is your pi home itself, so there is nothing to import. ${refusal}`, 1);
590
+ }
591
+ if (homeNeedsBootstrap(home.dir)) {
592
+ if (dryRun) {
593
+ process.stdout.write(`Would prepare ${home.dir} as a new Gentle Shell home\n`);
594
+ } else {
595
+ try {
596
+ await bootstrapOwnedHome(home);
597
+ } catch (error) {
598
+ fail(`gentle-shell home import-pi: could not prepare ${home.dir} (${error.message}); nothing was imported`, 1);
599
+ }
600
+ process.stdout.write(`Prepared ${home.dir} as a new Gentle Shell home\n`);
601
+ }
602
+ }
603
+ const result = importPi({ source, target: home.dir, dryRun });
604
+ for (const line of formatImportPiReport(result, { source, target: home.dir, dryRun })) process.stdout.write(`${line}\n`);
605
+ process.exitCode = result.failed ? 1 : 0;
606
+ }
607
+
630
608
  // Test/development-only override for the setup subcommand's gentle-ai
631
609
  // executable path. Lets a test point at a stub script (or a deliberately
632
610
  // missing path) without touching the real pinned .gentle-ai/v<version>/gentle-ai
@@ -799,12 +777,18 @@ function buildSetupEnv(home, runtime, piLauncherDir) {
799
777
  const env = bundledSetupEnv({ ...bundledEnv, PI_CODING_AGENT_DIR: home.dir, GENTLE_PI_AGENT_HOME: home.dir }, { dirs: bundledSetupDirs, platform: process.platform });
800
778
  return gentleAiIsolationEnv(env, { packageRoot });
801
779
  }
802
- return {
780
+ const env = {
803
781
  ...process.env,
804
782
  PI_CODING_AGENT_DIR: home.dir,
805
783
  GENTLE_PI_AGENT_HOME: home.dir,
806
784
  PATH: [...(piLauncherDir ? [piLauncherDir] : []), dirname(runtime.command), process.env.PATH ?? ""].join(delimiter),
807
785
  };
786
+ // #2201: the isolated home's own config folder, given to Gentle AI the
787
+ // bundled way (gentleAiIsolationEnv); never an outer session's marker.
788
+ const configHome = standaloneConfigHome(home);
789
+ if (configHome === undefined) delete env[GENTLE_SHELL_CONFIG_HOME_ENV];
790
+ else env[GENTLE_SHELL_CONFIG_HOME_ENV] = configHome;
791
+ return gentleAiIsolationEnv(env, { packageRoot });
808
792
  }
809
793
 
810
794
  // Gentle AI requires a `pi` on its PATH and runs `pi install npm:gentle-pi`
@@ -847,15 +831,34 @@ function sharedPersonaPath() {
847
831
  return join(homedir(), ".pi", "gentle-ai", "persona.json");
848
832
  }
849
833
 
850
- // The persona files a setup run may rewrite. Bundled (S9): the prefix's own,
851
- // which a Gentle AI with GENTLE_AI_PI_CONFIG_DIR (gentle-ai #5462) writes, and
852
- // still the user's, which a pinned Gentle AI without it writes; restoring an
853
- // unchanged file does nothing.
854
- function setupPersonaPaths() {
855
- if (!bundledInstall) return [sharedPersonaPath()];
856
- return [join(bundledGentleAiPaths(bundledInstall.root, process.platform).piConfig, "persona.json"), sharedPersonaPath()];
834
+ // The persona files a setup run may rewrite. Bundled (S9) and the standalone
835
+ // isolated home (#2201): its own config folder's, which a Gentle AI with
836
+ // GENTLE_AI_PI_CONFIG_DIR (gentle-ai #5462) writes, and still the user's,
837
+ // which a pinned Gentle AI without it writes; restoring an unchanged file
838
+ // does nothing.
839
+ function setupPersonaPaths(home) {
840
+ if (bundledInstall) return [join(bundledGentleAiPaths(bundledInstall.root, process.platform).piConfig, "persona.json"), sharedPersonaPath()];
841
+ const configHome = standaloneConfigHome(home);
842
+ return configHome === undefined ? [sharedPersonaPath()] : [join(configHome, "persona.json"), sharedPersonaPath()];
857
843
  }
858
844
 
845
+ // The real filesystem behind migrateStandaloneConfig (#2201).
846
+ const configMigrationFs = {
847
+ kind: (path) => {
848
+ try {
849
+ const stat = lstatSync(path);
850
+ return stat.isDirectory() ? "dir" : stat.isFile() ? "file" : "other";
851
+ } catch {
852
+ return "missing";
853
+ }
854
+ },
855
+ list: (path) => readdirSync(path),
856
+ mkdirPrivate: (path) => {
857
+ mkdirSync(path, { recursive: true, mode: 0o700 });
858
+ },
859
+ copyNew: (from, to) => copyFileSync(from, to, fsConstants.COPYFILE_EXCL),
860
+ };
861
+
859
862
  // Bundled setup (S9): the pinned Engram GENTLE_AI_ENGRAM_BIN names, published
860
863
  // once in runtime/ (verified, never brew or the user's PATH). A failure stops
861
864
  // setup rather than let Gentle AI acquire another Engram.
@@ -1123,7 +1126,7 @@ async function runSetupFlow(home, runtime, { dryRun, stdio, timeoutMs }) {
1123
1126
  if (!engram.ok) return engram;
1124
1127
  }
1125
1128
  // The memory decision never skips setup: it only selects the Engram
1126
- // environment (core or guard, policy, ENGRAM_URL) setup's children and the
1129
+ // environment (guard, policy, ENGRAM_URL) setup's children and the
1127
1130
  // launched Pi get. Gentle AI's `install --agent pi` and `pi-engram init` open
1128
1131
  // no store. Reclassified now that the verified core may just be published.
1129
1132
  if (bundledInstall) {
@@ -1134,8 +1137,10 @@ async function runSetupFlow(home, runtime, { dryRun, stdio, timeoutMs }) {
1134
1137
 
1135
1138
  const setupArgs = ["install", "--agent", "pi", "--scope", "global", ...(dryRun ? ["--dry-run"] : [])];
1136
1139
  const piLauncherDir = writeSetupPiLauncher(runtime);
1137
- const env = buildSetupEnv(home, runtime, piLauncherDir);
1138
- const personaSnapshots = setupPersonaPaths().map((path) => [path, safely("snapshot your Pi persona file", path, undefined, () => snapshotFile(path))]);
1140
+ // #2143: a Gentle AI with package exclusion never installs the launcher-owned
1141
+ // gentle-pi; an older one ignores this and runPostInstallCleanup removes it.
1142
+ const env = withLauncherPackagesExcluded(buildSetupEnv(home, runtime, piLauncherDir));
1143
+ const personaSnapshots = setupPersonaPaths(home).map((path) => [path, safely("snapshot your Pi persona file", path, undefined, () => snapshotFile(path))]);
1139
1144
  const statePath = sharedGentleAiStatePath();
1140
1145
  const originalStateText = safely("read your Gentle AI state file", statePath, undefined, () => readParsableJsonText(statePath));
1141
1146
  // Theme restore (never for --link — that home is the user's own
@@ -1387,6 +1392,25 @@ function writeHomeOwnershipMarker(home) {
1387
1392
  writeFileSync(homeOwnershipMarkerPath(home), `${content}\n`, "utf8");
1388
1393
  }
1389
1394
 
1395
+ // The isolated/--home bootstrap: the first launch's own (main, below) and
1396
+ // `home import-pi`'s before it adds files to a new or empty home.
1397
+ async function bootstrapOwnedHome(home) {
1398
+ // 0700: under umask 002 a default mode would leave ~/.gentle-shell group-writable,
1399
+ // which the bundled install's prefix claim must tighten or refuse.
1400
+ mkdirSync(home.dir, { recursive: true, mode: 0o700 });
1401
+ await installIsolatedTuiModeSetting(home.dir);
1402
+ writeHomeOwnershipMarker(home); // R3-001: lets a failed first auto-provision attempt still be retried later
1403
+ }
1404
+
1405
+ function homeNeedsBootstrap(dir) {
1406
+ if (!existsSync(dir)) return true;
1407
+ try {
1408
+ return isDirectory(dir) && readdirSync(dir).length === 0;
1409
+ } catch {
1410
+ return false;
1411
+ }
1412
+ }
1413
+
1390
1414
  // `homeHadContentBeforeBootstrap` must be read by the caller (main, below)
1391
1415
  // before the isolated/--home bootstrap runs: that bootstrap itself creates
1392
1416
  // and seeds a brand-new directory (settings.json with tuiMode, and now the
@@ -1580,7 +1604,9 @@ async function packageManagerAdapters() {
1580
1604
  fs: await import("node:fs/promises"),
1581
1605
  which: async (name) => findOnPath(name) ?? null,
1582
1606
  ...(invocation ? { invocation } : {}),
1583
- run: (command, argv, options = {}) => run(command, argv, { env: options.env ?? process.env, cwd: options.cwd, deadlineMs: options.deadlineMs ?? 20 * 60_000 }),
1607
+ // stderrTail: a caller that asks for it, such as a failed `update` install, gets the bounded stderr.
1608
+ run: (command, argv, options = {}) => run(command, argv, { env: options.env ?? process.env, cwd: options.cwd, deadlineMs: options.deadlineMs ?? 20 * 60_000,
1609
+ ...(options.stderrTail === undefined ? {} : { stderrTail: options.stderrTail }) }),
1584
1610
  };
1585
1611
  }
1586
1612
 
@@ -1679,6 +1705,26 @@ async function ensureBundledNpmCommand(bundled, home, homeHadContentBeforeBootst
1679
1705
  }
1680
1706
  }
1681
1707
 
1708
+ // The npmCommand a bundled install wrote into a home it owns must not outlive
1709
+ // it (#2469): Pi runs it for every package install, so a key naming a deleted
1710
+ // node.exe fails them all with ENOENT. A main-channel bootstrap drops the key
1711
+ // when it has the shape a bundled install writes and its node is gone; a
1712
+ // command the user set is never touched, and neither are an existing
1713
+ // executable, a link home and a foreign home.
1714
+ async function removeMissingNpmCommand(home, homeHadContentBeforeBootstrap) {
1715
+ if (home.mode === "link") return;
1716
+ const previous = provisionedEntry(readRawConfig(resolveConfigPath()), safeRealpath(home.dir));
1717
+ if (homeIsForeign(home, previous, homeHadContentBeforeBootstrap) || !isDirectory(home.dir)) return;
1718
+ const settingsPath = join(home.dir, "settings.json");
1719
+ const edit = withoutMissingBundledNpmCommand(readJsonIfExists(settingsPath), (path) => existsSync(path));
1720
+ if (edit.status === "malformed") {
1721
+ process.stderr.write(`gentle-shell: ${settingsPath} is not a JSON object, so a stale npmCommand in it is not repaired\n`);
1722
+ } else if (edit.status === "removed") {
1723
+ writeFileAtomically(settingsPath, edit.text);
1724
+ process.stderr.write(`gentle-shell: removed the bundled npmCommand naming a missing executable from ${settingsPath}; Pi installs packages with the npm on your PATH\n`);
1725
+ }
1726
+ }
1727
+
1682
1728
  // `gentle-shell self-uninstall` in a bundled install (S6, S7): the prefix's own
1683
1729
  // folders and exactly the PATH entry its receipt records; on Windows the running
1684
1730
  // node.exe and launcher are removed by a detached process after this one exits.
@@ -1736,11 +1782,27 @@ async function startupUpdate(bundled, args, startupEnv) {
1736
1782
  if (!shouldCheckStartupUpdate({ args, env: startupEnv, bundled: Boolean(bundled),
1737
1783
  inputTTY: process.stdin.isTTY === true, outputTTY: process.stdout.isTTY === true })) return undefined;
1738
1784
  const configPath = resolveConfigPath();
1739
- let skipped;
1740
- try { skipped = readRawConfig(configPath).skippedStartupRelease; } catch { /* A notice must not prevent opening. */ }
1785
+ let skipped, cache;
1786
+ try {
1787
+ const raw = readRawConfig(configPath);
1788
+ skipped = raw.skippedStartupRelease;
1789
+ // The last recorded lookup answers today's check without the network; a
1790
+ // stale or malformed record just means this launch looks again.
1791
+ cache = parseStartupReleaseCache(raw.startupReleaseCheck);
1792
+ } catch { /* A notice must not prevent opening. */ }
1741
1793
  const { latestRelease } = await import("../scripts/main-channel.mjs");
1742
- const offer = await findStartupUpdate({ current: ownPackageVersion(), skipped,
1743
- lookup: (signal) => latestRelease((url, options) => fetch(url, { ...options, signal })) });
1794
+ const offer = await findStartupUpdate({ current: ownPackageVersion(), skipped, cache,
1795
+ lookup: (signal) => latestRelease((url, options) => fetch(url, { ...options, signal })),
1796
+ // Only a live lookup reaches this: persisting it keeps the next launches
1797
+ // off the network. A failed write is retried by tomorrow's stale cache.
1798
+ cacheWrite: (latest) => {
1799
+ try {
1800
+ writeStartupReleaseCache(() => readJsonIfExists(configPath), (text) => {
1801
+ mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
1802
+ writeFileAtomically(configPath, text, { mode: 0o600 });
1803
+ }, latest);
1804
+ } catch { /* A notice must not prevent opening. */ }
1805
+ }, });
1744
1806
  if (!offer) return undefined;
1745
1807
  let choice;
1746
1808
  try { choice = await promptStartupUpdate(offer, { color: process.stdout.hasColors?.() === true }); }
@@ -1778,16 +1840,31 @@ async function main() {
1778
1840
  if (args.error !== undefined) fail(`${args.error}\nRun 'gentle-shell --help' for usage.`, 2);
1779
1841
  if (args.help) {
1780
1842
  process.stdout.write("Pi updates: update reinstalls the verified supplier of the Pi this launcher runs (a global Pi, or the\nShell package whose adjacent peer it is, at the same Shell version) and reads the resulting Pi version back.\nUnknown ownership refuses without installing; update --extensions-only stays Pi-only.\nPrefix-bundled Pi updates with gentle-shell upgrade.\n");
1781
- process.stdout.write(`${helpText()}\nBundled Engram safety: memory opens only a fresh, verified or Shell-provisioned store; otherwise ENGRAM_BIN\nis Shell's guard and memory stays off. Matching versions or updating alone do not prove compatibility.\nReuse requires a server with verified /compatibility evidence or matching Shell-provisioned store identity.\nBack up existing data before manual changes; do not delete it or bypass the safety check.\nShell rechecks on every launch and remains usable without memory.\nAn explicit ENGRAM_BIN or ENGRAM_URL is kept and bypasses the guard.\n`);
1843
+ process.stdout.write(`${helpText()}\nBundled Engram memory: Shell shares the global Engram server on 127.0.0.1:7437, like other agents' hooks.\nA running server is reused only with verified /compatibility evidence; when nothing listens, Shell starts the\nbundled core's engram serve (default ~/.engram) and waits for that evidence. Otherwise ENGRAM_BIN is Shell's\nguard and memory stays off for that launch. Matching versions or updating alone do not prove compatibility.\nBack up existing data before manual changes; do not delete it or bypass the safety check.\nShell rechecks on every launch and remains usable without memory.\nAn explicit ENGRAM_BIN or ENGRAM_URL is kept and bypasses the guard.\n`);
1782
1844
  process.exit(0);
1783
1845
  }
1784
1846
  if (args.command === "home") {
1785
- handleHomeCommand(args.commandArgs);
1847
+ if (args.commandArgs[0] === "import-pi") await handleImportPiCommand(args);
1848
+ else handleHomeCommand(args.commandArgs);
1786
1849
  return;
1787
1850
  }
1788
1851
  // The verification input applies to `--version` only (scripts/bundled-install.mjs upgradeBundled).
1789
1852
  const verifying = args.version && args.command === undefined ? process.env[BUNDLED_VERIFY_ENV] : undefined;
1790
1853
  const bundled = resolveBundledInstall({ packageRoot, platform: process.platform, fs: bundledFs, verifying });
1854
+ if (args.command === "memory") {
1855
+ if (args.commandArgs.length !== 2 || args.commandArgs[0] !== "migrate" || args.commandArgs[1] !== "--confirm-quiescent") {
1856
+ process.stderr.write(MIGRATION_USAGE);
1857
+ process.exitCode = 2;
1858
+ return;
1859
+ }
1860
+ if (!bundled) fail("gentle-shell: memory migrate requires a prefix-bundled installation", 1);
1861
+ bundledInstall = bundled;
1862
+ const result = migrateBundledEngram({ prefix: bundled.root, core: verifiedBundledCore(), home: homedir(), env: process.env });
1863
+ process.stdout.write(result.stdout);
1864
+ process.stderr.write(result.stderr);
1865
+ process.exitCode = result.code;
1866
+ return;
1867
+ }
1791
1868
  if (args.command === "upgrade") {
1792
1869
  if (bundled) process.exitCode = await handleBundledUpgrade(bundled, args.commandArgs);
1793
1870
  else await handleUpgradeCommand(args.commandArgs);
@@ -1810,7 +1887,7 @@ async function main() {
1810
1887
  if (home.mode === "path") home = { ...home, dir: resolvePath(home.dir) };
1811
1888
 
1812
1889
  // Version and help runs (Shell's own or forwarded to Pi) never probe Engram,
1813
- // ensure the companion, print memory notices or touch provenance.
1890
+ // ensure the companion, print memory notices or start a server.
1814
1891
  const engramQuiet = args.version || piVersionOrHelpRequested(args.passthrough);
1815
1892
  let runtime;
1816
1893
  if (bundled) {
@@ -1843,7 +1920,11 @@ async function main() {
1843
1920
  });
1844
1921
  if (versionProbe.error) fail(`Could not run the pi runtime at "${runtime.command}": ${versionProbe.error.message}`, 1);
1845
1922
  const versionCheck = checkPiVersion(versionProbe.stdout ?? "");
1846
- if (!versionCheck.ok) fail(versionCheck.message, 1);
1923
+ // A managed self update is how an old Pi gets replaced, so the minimum-version
1924
+ // gate must not block it; everything else still requires the minimum.
1925
+ const managedSelfUpdate = !bundled && args.piSubcommand === "update" && versionCheck.version !== undefined
1926
+ && (await import("../scripts/pi-update.mjs")).managedUpdatePlan(args.passthrough).self;
1927
+ if (!versionCheck.ok && !managedSelfUpdate) fail(versionCheck.message, 1);
1847
1928
 
1848
1929
  if (args.version) {
1849
1930
  process.stdout.write(`${describeVersion({ gentlePiVersion: ownPackageVersion(), piVersion: versionCheck.version, home })}\n`);
@@ -1869,7 +1950,7 @@ async function main() {
1869
1950
  const { managedUpdatePlan, updateSelectedPi } = await import("../scripts/pi-update.mjs");
1870
1951
  const plan = managedUpdatePlan(args.passthrough);
1871
1952
  if (plan.self) {
1872
- process.exitCode = await updateSelectedPi({ runtime, packageRoot, platform: process.platform, currentVersion: versionCheck.version, cwd: tmpdir(),
1953
+ process.exitCode = await updateSelectedPi({ runtime, packageRoot, platform: process.platform, currentVersion: versionCheck.version, cwd: tmpdir(), env: process.env, home: homedir(),
1873
1954
  adapters: await packageManagerAdapters(), out: (line) => process.stdout.write(`${line}\n`) });
1874
1955
  if (plan.piArgs === undefined) return;
1875
1956
  }
@@ -1893,12 +1974,22 @@ async function main() {
1893
1974
  // Isolated-home bootstrap: only on a home gentle-shell has not seen before
1894
1975
  // (link never bootstraps — it reuses the user's own pi agent home as-is).
1895
1976
  if ((home.mode === "isolated" || home.mode === "path") && !existsSync(home.dir)) {
1896
- // 0700: under umask 002 a default mode would leave ~/.gentle-shell group-writable,
1897
- // which the bundled install's prefix claim must tighten or refuse.
1898
- mkdirSync(home.dir, { recursive: true, mode: 0o700 });
1899
- await installIsolatedTuiModeSetting(home.dir);
1900
- writeHomeOwnershipMarker(home); // R3-001: lets a failed first auto-provision attempt still be retried later
1901
- process.stderr.write(`gentle-shell: using a separate home at ${home.dir}. Run 'gentle-shell --link' to reuse your pi sign-ins and chats.\n`);
1977
+ await bootstrapOwnedHome(home);
1978
+ const piSignIns = existsSync(join(userPiHome(process.env, homedir()), "auth.json"));
1979
+ process.stderr.write(`${separateHomeHint(home, { piSignIns })}\n`);
1980
+ }
1981
+
1982
+ // #2201: the standalone isolated home's own config folder, seeded once from
1983
+ // the shared ~/.pi/gentle-ai. Not when an explicit GENTLE_PI_CONFIG_HOME
1984
+ // decides the config home; never blocks the launch.
1985
+ const configHome = bundled || process.env.GENTLE_PI_CONFIG_HOME ? undefined : standaloneConfigHome(home);
1986
+ if (configHome !== undefined) {
1987
+ try {
1988
+ const migration = migrateStandaloneConfig({ source: join(homedir(), ".pi", "gentle-ai"), target: configHome, fs: configMigrationFs });
1989
+ for (const line of migration.lines) process.stderr.write(`${line}\n`);
1990
+ } catch (error) {
1991
+ process.stderr.write(`gentle-shell: could not prepare ${configHome} (${error.message}); continuing\n`);
1992
+ }
1902
1993
  }
1903
1994
 
1904
1995
  if (bundled) {
@@ -1907,6 +1998,12 @@ async function main() {
1907
1998
  } catch (error) {
1908
1999
  process.stderr.write(`gentle-shell: could not give Pi the bundled npm in ${join(home.dir, "settings.json")} (${error.message}); continuing\n`);
1909
2000
  }
2001
+ } else {
2002
+ try {
2003
+ await removeMissingNpmCommand(home, homeHadContentBeforeBootstrap);
2004
+ } catch (error) {
2005
+ process.stderr.write(`gentle-shell: could not repair the npmCommand in ${join(home.dir, "settings.json")} (${error.message}); continuing\n`);
2006
+ }
1910
2007
  }
1911
2008
 
1912
2009
  if (args.command === "setup") {
@@ -2040,7 +2137,7 @@ async function main() {
2040
2137
 
2041
2138
  if (bundled) {
2042
2139
  // Recheck after setup and before provider loading: never carry a stale
2043
- // fresh-store decision across an installer that could create data.
2140
+ // decision across an installer that could create data.
2044
2141
  if (args.piSubcommand === undefined && !engramQuiet) {
2045
2142
  bundledMemory = await classifyBundledMemory();
2046
2143
  bundledEnv = bundledProcessEnv();
@@ -2048,9 +2145,9 @@ async function main() {
2048
2145
  // could not be verified is reported, never a reason to stop Shell.
2049
2146
  const companion = await ensureEngramCompanion(home, runtime, ["ignore", 2, 2], 10000);
2050
2147
  if (!companion.ok) process.stderr.write(`${companion.message}\n`);
2051
- // A fresh store's identity and provenance exist before Pi starts, or
2052
- // memory stays off: Pi never runs with allow without a written record.
2053
- if (bundledMemory.fresh && !provisionEngramStore()) bundledMemory = { policy: "disabled", notice: ENGRAM_PROVISION_FAILED_NOTICE };
2148
+ // Only this final decision starts the shared server, and Pi gets its
2149
+ // URL only once the server verified.
2150
+ if (bundledMemory.spawn) bundledMemory = await startSharedEngram();
2054
2151
  if (bundledMemory.notice) process.stderr.write(`gentle-shell: ${bundledMemory.notice}\n`);
2055
2152
  }
2056
2153
  bundledEnv = bundledProcessEnv();
@@ -2066,6 +2163,7 @@ async function main() {
2066
2163
  passthrough: [...managedHerdrExtensionArgs(home, args), ...passthrough],
2067
2164
  piSubcommand: args.piSubcommand,
2068
2165
  baseEnv: bundledEnv ?? process.env,
2166
+ bundled: Boolean(bundled),
2069
2167
  homedir: homedir(),
2070
2168
  // The spawn below sets no cwd, so pi runs in the launcher's own.
2071
2169
  cwd: process.cwd(),
@@ -1,16 +1,17 @@
1
1
  {
2
2
  "schema": "gentle-pi.review-provider-contract-mirror-lock/v1",
3
3
  "acquisition": "field-test-local",
4
- "contract_semver": "1.3.0",
4
+ "contract_semver": "1.4.0",
5
5
  "source": {
6
6
  "kind": "archive",
7
- "archive_sha256": "b04b6b75d1a446c942a816fbf92932d16f8c3d3001f93159dee2a92bbf2ebe44"
7
+ "archive_sha256": "9d2927a84ba9a596f69029e3f854b16f86ca4c8b32cc5d63683764ccf2fb394b"
8
8
  },
9
- "tree_sha256": "a2b95892ec8f10340d8d1bb4fc805f9c92db000f58b60b382ff859d312fdc023",
9
+ "tree_sha256": "aab5ab7fe7ee6bb180d8bbce21c034cebb0c0d284afcc8664a9b3bb57323c35d",
10
10
  "entries": {
11
- "README.md": "18baab5ee79aefd0bc62a28da0dadcf2162544f57a940c5b859cd6bc4932a085",
12
- "manifest.json": "4e335a987e053dac42425ffe31628253eb43dd26f1235085fa90b200bce13d13",
11
+ "README.md": "3a655c2f19aee02636da062f0cae7a189b7e5f691c8ba8010f2ed50e5d8e8d70",
12
+ "manifest.json": "d2b5765e28df5b2459848379e91a27beb806916fabc96207060ad1cc5e911972",
13
13
  "orchestration/pi.md": "2d7ddd99f1c739a97f4a6b3929e5e2bdddcbf69aed69356395038d90bca25b2f",
14
+ "routing/pi.md": "1c7466e2333c08e386b4b5afb081531146d181030692463431e7695df7f90690",
14
15
  "schemas/lens.schema.json": "9b8a2354a570888486015434501b66b51441a3baf5bf858635e4fff56b2e8298",
15
16
  "schemas/refuter.schema.json": "36f8267c6c3f04600b8f1cb77df70d90858611af822cc81922efd6b3502fb646",
16
17
  "schemas/targeted-validator.schema.json": "40e397195bdfbc6782fc3090f20c5be082e4d14d2855314f99ecb8506133eddc",
@@ -19,8 +20,8 @@
19
20
  "vectors/targeted-validator.json": "7b5a5165e3a913863fb98d2ba5387811860c21333b5c37e6ed3b4e0d121cf2fa"
20
21
  },
21
22
  "generated": {
22
- "generated/provider-capabilities.baseline.json": "f439a3f83394219dae5257a88f112eba5e67f9d4e566eb6d9f3f5608b727c1f7",
23
- "generated/provider-roles.baseline.json": "71982937962834120b8304c795bf0e90c04b262a4293c6b96bf423b68a5781e0"
23
+ "generated/provider-capabilities.baseline.json": "bca708f5f67f506b4acb2cfdcb775b02a51468579fc7d6b4d169785ff411a215",
24
+ "generated/provider-roles.baseline.json": "aa36ec6f7469ac6faab756eec681f68f234cbadc1e12b345a93c6c34aa01f7aa"
24
25
  },
25
26
  "runtimes": [
26
27
  "claude-code",
@@ -19,4 +19,11 @@ It carries no executable content, same as every other file in this bundle: a
19
19
  runtime mirrors and delivers the text as-is and still relies on Go for every
20
20
  review decision, prompt, receipt, and delivery gate.
21
21
 
22
+ ## Routing
23
+
24
+ manifest.json's routing array lists, for closed runtimes only, one
25
+ routing/<runtime>.md file: the exact ODD implementation-routing block Gentle
26
+ AI renders for that runtime, whose prompt Gentle AI does not install. It is
27
+ data only as well: a runtime mirrors and delivers the text as-is.
28
+
22
29
  Go remains the admission authority for prompts, results, receipts, and delivery gates.