gentle-pi 4.2.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (334) hide show
  1. package/README.md +340 -138
  2. package/assets/agents/gentle-ai-explore.md +1 -0
  3. package/assets/agents/gentle-ai-verify.md +6 -4
  4. package/assets/agents/gentle-ai-worker.md +7 -1
  5. package/assets/design-canvas/canvas.css +648 -0
  6. package/assets/design-canvas/canvas.js +1497 -0
  7. package/assets/design-canvas/index.html +88 -0
  8. package/assets/design-canvas/inspector.js +290 -0
  9. package/assets/orchestrator-delegation-reference.md +163 -0
  10. package/assets/orchestrator-delegation.md +25 -176
  11. package/assets/orchestrator-tracking.md +1 -1
  12. package/assets/orchestrator-verification.md +14 -8
  13. package/assets/orchestrator-writer.md +5 -1
  14. package/assets/orchestrator.md +11 -11
  15. package/bin/engram-guard +1 -1
  16. package/bin/gentle-shell.mjs +237 -139
  17. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  18. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
  19. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
  20. package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
  21. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
  22. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
  23. package/docs/assets/diagrams/agent-orchestration.svg +54 -15
  24. package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
  25. package/docs/assets/diagrams/measured-routing.svg +49 -0
  26. package/docs/assets/diagrams/native-review.svg +52 -12
  27. package/docs/assets/diagrams/odd-workflow.svg +128 -66
  28. package/docs/assets/diagrams/permission-modes.svg +51 -0
  29. package/docs/assets/features/audio-notifications.png +0 -0
  30. package/docs/assets/features/command-palette.png +0 -0
  31. package/docs/bundled-install.md +65 -30
  32. package/docs/delegated-verification.md +16 -8
  33. package/docs/gentle-agents-activity.md +16 -6
  34. package/docs/gentle-music.md +61 -0
  35. package/docs/gentle-shell-uninstall.md +8 -1
  36. package/docs/gentle-shell.md +80 -9
  37. package/docs/install-wizard.md +37 -6
  38. package/docs/permission-modes.md +8 -3
  39. package/docs/readme-reference.md +120 -28
  40. package/docs/session-profile-format.md +26 -0
  41. package/docs/voice.md +259 -0
  42. package/docs/yolo-mode.md +38 -10
  43. package/extensions/ask-user-choice.ts +4 -1
  44. package/extensions/codegraph-tools.ts +68 -6
  45. package/extensions/gentle-agents.ts +270 -48
  46. package/extensions/gentle-ai.ts +490 -113
  47. package/extensions/gentle-design.ts +158 -0
  48. package/extensions/gentle-herdr-activity.ts +2 -3
  49. package/extensions/gentle-music.ts +289 -0
  50. package/extensions/gentle-shell.ts +359 -85
  51. package/extensions/gentle-todo.ts +43 -9
  52. package/extensions/gentle-voice.ts +11 -0
  53. package/extensions/quiet-tools.ts +1 -1
  54. package/extensions/session-scratch.ts +384 -0
  55. package/extensions/skill-registry.ts +63 -8
  56. package/extensions/startup-banner.ts +2 -3
  57. package/lib/agent-home.ts +8 -3
  58. package/lib/agent-profiles.ts +26 -0
  59. package/lib/agents-config.ts +35 -0
  60. package/lib/agents-protocol.ts +60 -1
  61. package/lib/agents-runner.ts +107 -9
  62. package/lib/agents-session-transport.ts +101 -6
  63. package/lib/agents-view.ts +3 -1
  64. package/lib/agents-widget.ts +14 -1
  65. package/lib/auto-permission-memory.ts +48 -18
  66. package/lib/auto-permission-runtime.ts +21 -6
  67. package/lib/bounded-writer-admission.ts +51 -0
  68. package/lib/clipboard-image.ts +147 -0
  69. package/lib/command-palette-catalog.ts +2 -4
  70. package/lib/commit-slice-signal.ts +100 -0
  71. package/lib/context-backstop.ts +120 -0
  72. package/lib/delivered-test-lint.ts +80 -0
  73. package/lib/design-canvas-server.ts +947 -0
  74. package/lib/design-comments.ts +437 -0
  75. package/lib/design-handoffs.ts +313 -0
  76. package/lib/design-open-browser.ts +58 -0
  77. package/lib/design-sources.ts +120 -0
  78. package/lib/destructive-command-guard.ts +120 -9
  79. package/lib/external-editor.ts +31 -0
  80. package/lib/gauge-theme.ts +146 -0
  81. package/lib/gentle-ai-binary.ts +28 -7
  82. package/lib/gentle-design.ts +513 -0
  83. package/lib/gentle-shell-bundled.ts +146 -36
  84. package/lib/gentle-shell-import-pi.ts +233 -0
  85. package/lib/gentle-shell-launcher.ts +154 -15
  86. package/lib/gentle-shell-startup-update.ts +61 -9
  87. package/lib/gentle-shell-uninstall.ts +51 -2
  88. package/lib/herdr-activity.ts +24 -0
  89. package/lib/image-attachments.ts +150 -0
  90. package/lib/inprocess-reviewer.ts +33 -2
  91. package/lib/nan-provider.ts +3 -0
  92. package/lib/native-review-cli.ts +20 -5
  93. package/lib/notification-audio-native.ts +149 -13
  94. package/lib/notification-audio.ts +4 -0
  95. package/lib/notification-pulse-worker.ts +82 -18
  96. package/lib/notification-service.ts +5 -1
  97. package/lib/orchestrator-discovery.ts +5 -1
  98. package/lib/orchestrator-presence.ts +87 -8
  99. package/lib/orchestrator-scope.ts +31 -3
  100. package/lib/orchestrator-state.ts +11 -3
  101. package/lib/permission-mode-policy.ts +2 -2
  102. package/lib/provider-contract-bundle.ts +99 -53
  103. package/lib/review-candidate-view-owner.ts +29 -19
  104. package/lib/review-candidate-view.ts +36 -2
  105. package/lib/review-host-relay.ts +11 -2
  106. package/lib/review-reminder-receipt.ts +24 -1
  107. package/lib/review-risk-assessment.ts +43 -38
  108. package/lib/review-session-standing-permission.ts +3 -1
  109. package/lib/session-profile-append-controller.ts +713 -0
  110. package/lib/session-profile-disk-reader.ts +182 -0
  111. package/lib/session-profile-freeze.ts +30 -1
  112. package/lib/session-profile-persistence.ts +15 -7
  113. package/lib/session-scratch.ts +523 -0
  114. package/lib/session-worktree-registry.ts +7 -0
  115. package/lib/shell-bar.ts +29 -28
  116. package/lib/shell-changes-view.ts +80 -13
  117. package/lib/shell-changes.ts +81 -5
  118. package/lib/shell-gauge.ts +49 -6
  119. package/lib/shell-prompt.ts +4 -4
  120. package/lib/shell-sidebar-layout.ts +26 -6
  121. package/lib/shell-todo.ts +24 -5
  122. package/lib/shell-usage-view.ts +5 -1
  123. package/lib/shell-usage.ts +7 -7
  124. package/lib/verifier-admission.ts +30 -0
  125. package/lib/vim-editor-adapter.ts +11 -8
  126. package/lib/voice-artifacts.ts +188 -0
  127. package/lib/voice-command.ts +576 -0
  128. package/lib/voice-install.ts +455 -0
  129. package/lib/voice-platform.ts +99 -0
  130. package/lib/voice-push-to-talk.ts +262 -0
  131. package/lib/voice-session.ts +460 -0
  132. package/lib/voice-settings.ts +50 -0
  133. package/lib/voice-ui.ts +158 -0
  134. package/lib/yolo-session-policy.ts +58 -22
  135. package/lib/yolo-temp-scratch.ts +69 -0
  136. package/package.json +1 -2
  137. package/runtime/gentle-ai-binary.mjs +28 -7
  138. package/runtime/gentle-shell-bundled.mjs +145 -35
  139. package/runtime/gentle-shell-import-pi.mjs +234 -0
  140. package/runtime/gentle-shell-launcher.mjs +152 -13
  141. package/runtime/gentle-shell-startup-update.mjs +61 -9
  142. package/runtime/gentle-shell-uninstall.mjs +51 -2
  143. package/runtime/native-review-cli.mjs +19 -4
  144. package/runtime/review-risk-assessment.mjs +43 -38
  145. package/scripts/build-runtime-modules.mjs +1 -0
  146. package/scripts/bundled-engram-migration.mjs +119 -0
  147. package/scripts/gentle-ai-installer.mjs +10 -10
  148. package/scripts/installer-downloads.mjs +7 -7
  149. package/scripts/installer-preflight.mjs +5 -3
  150. package/scripts/installer-server.mjs +14 -4
  151. package/scripts/installer-windows.mjs +37 -7
  152. package/scripts/main-channel.mjs +94 -19
  153. package/scripts/pi-update.mjs +34 -6
  154. package/scripts/run-test-suite.mjs +5 -0
  155. package/scripts/verify-package-files.mjs +43 -13
  156. package/scripts/voice-worker.cjs +476 -0
  157. package/skills/design/SKILL.md +110 -0
  158. package/skills/gentle-ai/SKILL.md +3 -3
  159. package/skills/work-unit-commits/SKILL.md +5 -3
  160. package/tests/agent-profiles.test.ts +24 -0
  161. package/tests/agent-risk-escalation.test.ts +37 -10
  162. package/tests/agents-config.test.ts +61 -0
  163. package/tests/agents-fake-child.ts +8 -1
  164. package/tests/agents-protocol.test.ts +32 -0
  165. package/tests/agents-runner.test.ts +485 -7
  166. package/tests/agents-session-transport.test.ts +53 -3
  167. package/tests/ask-user-choice.test.ts +30 -0
  168. package/tests/auto-mode.test.ts +17 -0
  169. package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
  170. package/tests/auto-permission-runtime.test.ts +85 -1
  171. package/tests/autonomous-guard.test.ts +142 -0
  172. package/tests/bounded-writer-admission.test.ts +11 -3
  173. package/tests/bundled-install.test.ts +3 -3
  174. package/tests/card-content-policy.test.ts +19 -9
  175. package/tests/child-safety.test.ts +39 -0
  176. package/tests/clipboard-image.test.ts +114 -0
  177. package/tests/codegraph-tools.test.ts +123 -2
  178. package/tests/command-palette.test.ts +15 -5
  179. package/tests/commit-slice-signal.test.ts +162 -0
  180. package/tests/context-backstop.test.ts +225 -0
  181. package/tests/delegate-for-reason-contract.test.ts +92 -24
  182. package/tests/delivered-test-lint.test.ts +105 -0
  183. package/tests/design-canvas-assets.test.ts +710 -0
  184. package/tests/design-canvas-server.test.ts +1096 -0
  185. package/tests/design-comments.test.ts +201 -0
  186. package/tests/design-handoffs.test.ts +282 -0
  187. package/tests/design-open-browser.test.ts +70 -0
  188. package/tests/design-sources.test.ts +132 -0
  189. package/tests/destructive-command-guard.test.ts +85 -1
  190. package/tests/engram-safety.test.ts +132 -57
  191. package/tests/external-editor.test.ts +84 -0
  192. package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
  193. package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
  194. package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
  195. package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
  196. package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
  197. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
  198. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
  199. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
  200. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
  201. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
  202. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
  203. package/tests/gauge-theme.test.ts +120 -0
  204. package/tests/gentle-agents.test.ts +645 -21
  205. package/tests/gentle-ai-binary.test.ts +1 -1
  206. package/tests/gentle-ai-child-guards.test.ts +61 -0
  207. package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
  208. package/tests/gentle-ai-installer.test.ts +51 -51
  209. package/tests/gentle-ai.test.ts +292 -13
  210. package/tests/gentle-design.test.ts +1313 -0
  211. package/tests/gentle-music.test.ts +335 -0
  212. package/tests/gentle-shell-bin.test.ts +163 -0
  213. package/tests/gentle-shell-bundled.test.ts +392 -176
  214. package/tests/gentle-shell-import-pi.test.ts +262 -0
  215. package/tests/gentle-shell-launcher.test.ts +107 -2
  216. package/tests/gentle-shell-startup-update.test.ts +77 -1
  217. package/tests/gentle-shell-uninstall.test.ts +89 -0
  218. package/tests/gentle-shell.test.ts +225 -17
  219. package/tests/gentle-todo.test.ts +190 -0
  220. package/tests/headless-delivery-question.test.ts +58 -0
  221. package/tests/herdr-activity.test.ts +34 -1
  222. package/tests/image-attachments.test.ts +119 -0
  223. package/tests/image-paste-chips.test.ts +236 -0
  224. package/tests/inprocess-reviewer.test.ts +55 -2
  225. package/tests/installer-bundles.test.ts +12 -6
  226. package/tests/installer-posix-bootstrap.test.ts +8 -8
  227. package/tests/installer-preflight.test.ts +16 -0
  228. package/tests/installer-server.test.ts +10 -2
  229. package/tests/installer-windows-bootstrap.test.ts +94 -7
  230. package/tests/installer-windows-diagnostics.test.ts +111 -0
  231. package/tests/main-channel-upgrade.test.ts +232 -2
  232. package/tests/main-channel.test.ts +140 -21
  233. package/tests/nan-provider.test.ts +36 -0
  234. package/tests/native-review-capability-contract.test.ts +17 -4
  235. package/tests/native-review-parity.test.ts +23 -2
  236. package/tests/no-self-pause-contract.test.ts +5 -2
  237. package/tests/notification-audio-native.test.ts +128 -9
  238. package/tests/notification-customize.test.ts +17 -5
  239. package/tests/odd-integration.test.ts +5 -1
  240. package/tests/odd-routing-canonical-ratchet.test.ts +128 -162
  241. package/tests/odd-routing-contract.test.ts +115 -30
  242. package/tests/orchestrator-budget.test.ts +22 -8
  243. package/tests/orchestrator-discovery.test.ts +29 -0
  244. package/tests/orchestrator-presence.test.ts +100 -2
  245. package/tests/orchestrator-rdd-ownership.test.ts +3 -1
  246. package/tests/orchestrator-scope.test.ts +80 -0
  247. package/tests/orchestrator-state.test.ts +29 -0
  248. package/tests/orchestrator-status-publication-contract.test.ts +12 -8
  249. package/tests/package-files.test.ts +34 -0
  250. package/tests/package-manifest.test.ts +24 -20
  251. package/tests/permission-mode-policy.test.ts +10 -1
  252. package/tests/pi-update-launcher.test.ts +21 -2
  253. package/tests/pi-update.test.ts +99 -4
  254. package/tests/primary-session-prompt.test.ts +101 -0
  255. package/tests/provider-contract-bundle.test.ts +118 -0
  256. package/tests/provider-contract-mirror.test.ts +24 -0
  257. package/tests/quiet-tool-rendering.test.ts +23 -0
  258. package/tests/rdd-aware-verification-contract.test.ts +29 -11
  259. package/tests/rdd-status-line.test.ts +79 -0
  260. package/tests/review-agent-end-preflight.test.ts +6 -0
  261. package/tests/review-candidate-view.test.ts +49 -0
  262. package/tests/review-contract-prompt.test.ts +5 -2
  263. package/tests/review-controller-native-routing.test.ts +14 -1
  264. package/tests/review-controller.test.ts +1 -1
  265. package/tests/review-host-relay-routing.test.ts +147 -3
  266. package/tests/review-host-relay.test.ts +9 -2
  267. package/tests/review-json-arguments.test.ts +20 -3
  268. package/tests/review-loadout-rdd-off-sdk.test.ts +258 -0
  269. package/tests/review-owner-identity.test.ts +132 -0
  270. package/tests/review-relay-transport-agent.test.ts +26 -0
  271. package/tests/review-reminder-receipt.test.ts +31 -1
  272. package/tests/review-risk-assessment.test.ts +163 -76
  273. package/tests/review-session-standing-permission-controller.test.ts +14 -1
  274. package/tests/review-session-standing-permission.test.ts +27 -1
  275. package/tests/run-test-suite.test.ts +15 -0
  276. package/tests/runtime-harness.mjs +16 -6
  277. package/tests/session-changes-shell.test.ts +1 -1
  278. package/tests/session-changes.test.ts +2 -1
  279. package/tests/session-profile-append-controller-fork.test.ts +458 -0
  280. package/tests/session-profile-append-controller-pi.test.ts +290 -0
  281. package/tests/session-profile-append-controller.test.ts +731 -0
  282. package/tests/session-profile-disk-reader-pi.test.ts +183 -0
  283. package/tests/session-profile-disk-reader.test.ts +311 -0
  284. package/tests/session-profile-freeze.test.ts +23 -0
  285. package/tests/session-scratch.test.ts +1011 -0
  286. package/tests/session-subject-routing-contract.test.ts +2 -1
  287. package/tests/session-worktree-registry.test.ts +13 -1
  288. package/tests/shell-bar.test.ts +21 -2
  289. package/tests/shell-changes-view.test.ts +137 -1
  290. package/tests/shell-changes.test.ts +148 -1
  291. package/tests/shell-gauge.test.ts +42 -0
  292. package/tests/shell-prompt.test.ts +3 -3
  293. package/tests/shell-render-cache.test.ts +134 -1
  294. package/tests/shell-sidebar-layout.test.ts +50 -0
  295. package/tests/short-check-routing-contract.test.ts +1 -1
  296. package/tests/skill-collision-prefixes.test.ts +3 -1
  297. package/tests/skill-registry-watcher.test.ts +17 -5
  298. package/tests/skill-registry.test.ts +47 -3
  299. package/tests/small-task-inline-contract.test.ts +37 -0
  300. package/tests/standalone-config-home.test.ts +324 -0
  301. package/tests/startup-banner.test.ts +5 -4
  302. package/tests/startup-home-repo.test.ts +34 -0
  303. package/tests/support/orchestrator-modules.ts +1 -0
  304. package/tests/task-size-routing-contract.test.ts +70 -10
  305. package/tests/test-scratch-contract.test.ts +102 -0
  306. package/tests/test-strength-contract.test.ts +36 -1
  307. package/tests/tool-batching-contract.test.ts +64 -0
  308. package/tests/verification-proportionality.test.ts +46 -0
  309. package/tests/verifier-admission.test.ts +154 -0
  310. package/tests/verify-agent-contract.test.ts +8 -8
  311. package/tests/voice-command.test.ts +773 -0
  312. package/tests/voice-install.test.ts +323 -0
  313. package/tests/voice-platform.test.ts +176 -0
  314. package/tests/voice-push-to-talk.test.ts +444 -0
  315. package/tests/voice-session.test.ts +434 -0
  316. package/tests/voice-settings.test.ts +49 -0
  317. package/tests/voice-ui.test.ts +97 -0
  318. package/tests/voice-worker.test.ts +445 -0
  319. package/tests/weak-model-economy-prompt.test.ts +32 -0
  320. package/tests/windows-hidden-processes.test.ts +30 -2
  321. package/tests/writer-edit-surface-scope.test.ts +3 -0
  322. package/tests/writer-feature-document-admission.test.ts +156 -0
  323. package/tests/yolo-customize.test.ts +5 -1
  324. package/tests/yolo-mode.test.ts +19 -1
  325. package/tests/yolo-session-policy-capture.test.ts +104 -0
  326. package/tests/yolo-temp-scratch.test.ts +71 -0
  327. package/scripts/mirror-odd-routing.mjs +0 -242
  328. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
  329. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
  330. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
  331. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  332. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
  333. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
  334. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -1,7 +1,9 @@
1
1
  // Generated by scripts/build-runtime-modules.mjs. Do not edit.
2
- import { lstatSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmdirSync, rmSync } from "node:fs";
2
+ import { lstatSync, readdirSync, readFileSync, readlinkSync, realpathSync, renameSync, rmdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { randomUUID } from "node:crypto";
3
4
  import { createInterface } from "node:readline";
4
5
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
6
+ import { withoutBundledNpmCommand } from "./gentle-shell-bundled.mjs";
5
7
  import { isolatedDir, launcherConfigPath, parseRawLauncherConfig, provisionedEntry, userPiHome } from "./gentle-shell-launcher.mjs";
6
8
 
7
9
  // `gentle-shell self-uninstall`: removes what Gentle Shell itself created, then
@@ -264,6 +266,14 @@ async function planSelfUninstall(input , configHome ,
264
266
  { path: gentleAiState, label: "Gentle AI's state", inside: true },
265
267
  ];
266
268
  const defaultConfigHome = canonical(join(homedir, ".pi", "gentle-ai"));
269
+ // #2201: the standalone launcher keeps the isolated home's config in a
270
+ // gentle-ai folder next to it (standaloneConfigHome). Next to the default
271
+ // home it lies inside ~/.gentle-shell and is Gentle Shell's by location;
272
+ // next to a GENTLE_SHELL_HOME it may be anyone's, so it is only listed.
273
+ // When GENTLE_PI_CONFIG_HOME names it, the config home's rules apply instead.
274
+ const defaultHome = resolve(isolated) === resolve(join(homedir, ".gentle-shell", "agent"));
275
+ const ownConfig = join(dirname(isolated), "gentle-ai");
276
+ const ownConfigSeparate = comparable(canonical(ownConfig), platform) !== comparable(canonical(configHome), platform);
267
277
  const candidates = [
268
278
  {
269
279
  who: env.GENTLE_SHELL_HOME ? `GENTLE_SHELL_HOME points at ${isolated}` : `The isolated home ${isolated}`,
@@ -279,6 +289,12 @@ async function planSelfUninstall(input , configHome ,
279
289
  allowedInsidePi: defaultConfigHome,
280
290
  },
281
291
  { who: `The launcher folder ${shellDirectory}`, path: shellDirectory, guards, allowedInsidePi: undefined },
292
+ ...(defaultHome && ownConfigSeparate ? [{
293
+ who: `Gentle Shell's config folder ${ownConfig}`,
294
+ path: ownConfig,
295
+ guards: [...guards, { path: configHome, label: "the Gentle AI config home", inside: true }],
296
+ allowedInsidePi: undefined,
297
+ }] : []),
282
298
  ];
283
299
  for (const candidate of candidates) {
284
300
  const real = canonical(candidate.path);
@@ -313,11 +329,14 @@ async function planSelfUninstall(input , configHome ,
313
329
 
314
330
  // The default isolated home is Gentle Shell's by location; a GENTLE_SHELL_HOME
315
331
  // directory only with the launcher's ownership marker or setup record.
316
- const defaultHome = resolve(isolated) === resolve(join(homedir, ".gentle-shell", "agent"));
317
332
  const owned = defaultHome || inspect(join(isolated, HOME_OWNERSHIP_MARKER), "file") === "ok"
318
333
  || provisionedEntry(launcherConfig, isolatedReal) !== undefined;
319
334
  if (owned || inspect(isolated, "dir") !== "ok") consider({ path: isolated, label: "isolated home: its sign-ins, chats and settings", kind: "dir" });
320
335
  else plan.kept.push(`${isolated}: not created by Gentle Shell (no ${HOME_OWNERSHIP_MARKER} marker or setup record)`);
336
+ if (defaultHome && ownConfigSeparate) consider({ path: ownConfig, label: "Gentle Shell's own configuration: profiles, banner and other preferences", kind: "dir" });
337
+ else if (ownConfigSeparate && inspect(ownConfig, "dir") !== "missing") {
338
+ plan.kept.push(`${ownConfig}: next to GENTLE_SHELL_HOME, so it may not be Gentle Shell's; remove it yourself if it holds only Gentle Shell settings`);
339
+ }
321
340
  consider({ path: configPath, label: "launcher settings", kind: "file" });
322
341
 
323
342
  for (const name of SHARED_CONFIG_FILES) {
@@ -762,6 +781,18 @@ function printBundledPlan(plan , input , o
762
781
  }
763
782
  }
764
783
 
784
+ /** Replaces `path`'s text through a same-directory temporary file, so a crash never leaves it half-written. */
785
+ function writeTextAtomically(path , text ) {
786
+ const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`);
787
+ try {
788
+ writeFileSync(temporary, text);
789
+ renameSync(temporary, path);
790
+ } catch (error) {
791
+ try { unlinkSync(temporary); } catch { /* The temporary file is best effort. */ }
792
+ throw error;
793
+ }
794
+ }
795
+
765
796
  /**
766
797
  * Runs `gentle-shell self-uninstall` for the bundled install at `root`. Exit
767
798
  * codes as runSelfUninstall: 0 done or dry run, 1 refused, cancelled or failed,
@@ -821,6 +852,24 @@ export async function runBundledSelfUninstall(input )
821
852
  } catch {
822
853
  // A leftover empty folder is harmless.
823
854
  }
855
+ // A home that stays must not keep Pi pointed at the npm this uninstall removed
856
+ // (#2469): an `npmCommand` naming the prefix fails every later `pi install`
857
+ // with ENOENT naming the deleted node.exe.
858
+ if (!confirmation.includeShared) {
859
+ const settingsPath = join(plan.shellHome, "agent", "settings.json");
860
+ let settingsText ;
861
+ try { settingsText = readFileSync(settingsPath, "utf8"); } catch { settingsText = undefined; }
862
+ const removal = withoutBundledNpmCommand(settingsText, (path) => within(root, path, platform));
863
+ if (removal.status === "removed") {
864
+ try {
865
+ writeTextAtomically(settingsPath, removal.text);
866
+ out(`Removed npmCommand from ${settingsPath}: it named the removed install.`);
867
+ } catch (error) {
868
+ err(`gentle-shell self-uninstall: could not remove npmCommand from ${settingsPath}: ${error instanceof Error ? error.message : String(error)}`);
869
+ err(`Pi package installs keep failing until the npmCommand naming ${root} is deleted from it.`);
870
+ }
871
+ }
872
+ }
824
873
  if (plan.held.length > 0) {
825
874
  try {
826
875
  if (!input.deferRemoval) throw new Error("no deferred removal on this system");
@@ -124,6 +124,7 @@ export const NATIVE_REVIEW_ERROR_CODE = {
124
124
 
125
125
 
126
126
 
127
+
127
128
  export const NATIVE_REVIEW_MODE_OPERATION = {
128
129
  STATUS: "status",
129
130
  ENABLE: "enable",
@@ -202,10 +203,10 @@ export const NATIVE_REVIEW_MODE_SCOPE = {
202
203
  // stream while still succeeding. That last qualifier is what keeps this list
203
204
  // honest: v2.4.0 deleted reviewConsentSkippedDefaultProvenance ("Reviews are
204
205
  // on by default; this was never explicitly chosen. ..."), which used to ride
205
- // with the skip notice whenever the resolved mode source was `default`. Under
206
- // opt-in receipt-driven development a default-source clone is refused long
207
- // before the consent ceremony runs, so the pinned binary can no longer emit
208
- // that line and it is removed here rather than left as dead tolerance. Multi-
206
+ // with the skip notice whenever the resolved mode source was `default`. Reviews
207
+ // are on by default again since gentle-ai v4.0.0, but the pinned binary no
208
+ // longer has that function and cannot emit the line, so it is removed here
209
+ // rather than left as dead tolerance. Multi-
209
210
  // line stderr is still expected in principle — these are separate Fprintln
210
211
  // calls, never one joined string — which is why membership is per line.
211
212
  export const REVIEW_CONSENT_NOTICES = Object.freeze([
@@ -1083,6 +1084,20 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
1083
1084
  // b04b6b75d1a446c942a816fbf92932d16f8c3d3001f93159dee2a92bbf2ebe44.
1084
1085
  // No new negotiated capability identity is asserted.
1085
1086
  "4.1.1": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
1087
+ // v4.2.0 repeats 4.1.1: the published binary still advertises
1088
+ // capabilities/v2.6 under review-integration/v2. The provider contract moved
1089
+ // from 1.3.0 to 1.4.0 (routing/pi.md, gentle-ai#5536), with published
1090
+ // tarball SHA-256
1091
+ // 9d2927a84ba9a596f69029e3f854b16f86ca4c8b32cc5d63683764ccf2fb394b.
1092
+ // No new negotiated capability identity is asserted.
1093
+ "4.2.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
1094
+ // v4.3.1 repeats 4.2.0: the published binary still advertises
1095
+ // capabilities/v2.6 under review-integration/v2, and the provider contract
1096
+ // stays at 1.4.0, with published tarball SHA-256
1097
+ // 58450471280aeb357443d6a28c819132d8c8eb78dd25b12bac64296b2f8c27c2.
1098
+ // The v4.3.0 tag has no release and is never pinned.
1099
+ // No new negotiated capability identity is asserted.
1100
+ "4.3.1": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
1086
1101
  });
1087
1102
 
1088
1103
 
@@ -268,8 +268,9 @@ export const VERIFICATION_TIER = {
268
268
  // ---------------------------------------------------------------------------
269
269
  // Agent-raised risk (gentle-shell#1494). The agent that made the change may
270
270
  // raise its risk to high by citing one item of the high-risk list in the
271
- // always-on Task Size section; it can never lower a tier. Lowering stays with
272
- // the deterministic native assessment.
271
+ // always-on Task Size section; it can never lower a tier. That escalation is
272
+ // the only way a plan gets an independent verifier: a native high or a failed
273
+ // assessment informs but never adds one by itself.
273
274
  // ---------------------------------------------------------------------------
274
275
 
275
276
  export const HIGH_RISK_ITEMS = Object.freeze({
@@ -306,10 +307,17 @@ export function decodeAgentRiskEscalation(value ) {
306
307
  return Object.freeze({ item: item , reason });
307
308
  }
308
309
 
309
- /** Raises passive or medium to high when the agent escalated; never lowers. */
310
- export function escalatedRisk(risk , escalation ) {
311
- if (escalation === undefined) return risk;
312
- return risk === VERIFICATION_TIER.PASSIVE || risk === VERIFICATION_TIER.MEDIUM ? VERIFICATION_TIER.HIGH : risk;
310
+ /**
311
+ * The risk tier the verification plan follows. The independent verifier is
312
+ * for concrete occasions only: the plan is high only when the agent escalated
313
+ * a named Task Size high-risk item. Without an escalation a native passive
314
+ * stays passive, and every other native tier -- medium, high, or unassessable
315
+ * (a failed or unavailable assessment) -- is medium: it informs, but adds no
316
+ * verifier by itself. The native tier stays visible to callers as `nativeRisk`.
317
+ */
318
+ export function verificationRisk(nativeRisk , escalation ) {
319
+ if (escalation !== undefined) return VERIFICATION_TIER.HIGH;
320
+ return nativeRisk === VERIFICATION_TIER.PASSIVE ? VERIFICATION_TIER.PASSIVE : VERIFICATION_TIER.MEDIUM;
313
321
  }
314
322
 
315
323
  export const RDD_LINE = {
@@ -357,6 +365,8 @@ export const NATIVE_REVIEW_OUTCOME = {
357
365
 
358
366
 
359
367
 
368
+
369
+
360
370
 
361
371
 
362
372
 
@@ -367,7 +377,7 @@ export const NATIVE_REVIEW_OUTCOME = {
367
377
  * naming which branch fired, up to and including the word "and"; the
368
378
  * tier-specific clause is appended after it.
369
379
  */
370
- function riskGatedPlan(gatePrefix , risk , writerProfile ) {
380
+ function riskGatedPlan(gatePrefix , risk ) {
371
381
  if (risk === VERIFICATION_TIER.PASSIVE) {
372
382
  return Object.freeze({
373
383
  writerSelfVerification: false,
@@ -377,26 +387,22 @@ function riskGatedPlan(gatePrefix , risk , writerProfile
377
387
  });
378
388
  }
379
389
 
380
- if (risk === VERIFICATION_TIER.MEDIUM) {
381
- const smallModelBias = writerProfile === WRITER_PROFILE.SMALL;
390
+ if (risk === VERIFICATION_TIER.HIGH) {
382
391
  return Object.freeze({
383
392
  writerSelfVerification: true,
384
393
  structuralReadbackOnly: false,
385
- independentVerifier: smallModelBias,
386
- reason: smallModelBias
387
- ? `${gatePrefix} the native assessment reports medium risk, but the small-model bias raises it one tier to high for verification purposes: the writer self-verifies and a separate independent verifier also runs.`
388
- : `${gatePrefix} the native assessment reports medium risk with a large writer profile: the writer's self-verification stands and no separate verifier is required.`,
394
+ independentVerifier: true,
395
+ reason: `${gatePrefix} an agent escalation names a Task Size high-risk item: the writer self-verifies and a separate independent verifier runs.`,
389
396
  });
390
397
  }
391
398
 
392
- // high or unassessable.
399
+ // medium, or an unassessable tier that did not go through verificationRisk:
400
+ // both inform only.
393
401
  return Object.freeze({
394
402
  writerSelfVerification: true,
395
403
  structuralReadbackOnly: false,
396
- independentVerifier: true,
397
- reason: risk === VERIFICATION_TIER.UNASSESSABLE
398
- ? `${gatePrefix} the native risk assessment could not be produced, so the candidate is treated as high risk: the writer self-verifies and a separate independent verifier always runs.`
399
- : `${gatePrefix} the native assessment reports high risk: the writer self-verifies and a separate independent verifier always runs.`,
404
+ independentVerifier: false,
405
+ reason: `${gatePrefix} no Task Size high-risk item was escalated: the writer's self-verification stands and no separate verifier runs, whatever the native tier or the model that wrote the change.`,
400
406
  });
401
407
  }
402
408
 
@@ -417,24 +423,23 @@ function nonClosedOutcomeClause(outcome ) {
417
423
 
418
424
  /**
419
425
  * Computes who verifies a delegated writer's change. Verification follows the
420
- * native risk tier whatever the RDD line and native review outcome
426
+ * plan risk tier whatever the RDD line and native review outcome
421
427
  * (verify-always-rdd-high S1, superseding the gentle-pi#662/#668 on-path): the
422
428
  * native review is an additional outside view and never replaces or skips the
423
429
  * tier's verification. `nativeReviewOutcome` only shapes the reason text.
424
430
  *
431
+ * `risk` is the plan risk from `verificationRisk`; `writerProfile` is never
432
+ * consulted, so the model or effort that wrote the change never adds a
433
+ * verifier.
434
+ *
425
435
  * - `passive`: structural readback by the parent only; no verifier, no tests.
426
- * - `medium`: the writer self-verifies; a separate independent verifier runs
427
- * only when `writerProfile` is `"small"` -- the small-model bias raises the
428
- * tier by one for verification purposes (medium -> high). An unknown or
429
- * omitted writer profile resolves to `"small"` before it reaches this
430
- * function (see `resolveWriterProfile`), so an unrecognized profile never
431
- * lowers a tier below what a known small model would get.
432
- * - `high` or `unassessable` (a failed or unrecognized native assessment is
433
- * always treated as high): the writer self-verifies and an independent
434
- * verifier always runs.
436
+ * - `medium` (also an `unassessable` tier passed directly): the writer
437
+ * self-verifies; no separate verifier.
438
+ * - `high` (only through an agent escalation naming a Task Size high-risk
439
+ * item): the writer self-verifies and an independent verifier runs.
435
440
  */
436
441
  export function verificationPlan(input ) {
437
- const { rddLine, risk, writerProfile } = input;
442
+ const { rddLine, risk } = input;
438
443
 
439
444
  if (rddLine === RDD_LINE.ON) {
440
445
  const outcome = input.nativeReviewOutcome ?? NATIVE_REVIEW_OUTCOME.UNKNOWN;
@@ -444,14 +449,13 @@ export function verificationPlan(input )
444
449
  return riskGatedPlan(
445
450
  `receipt-driven development is on and ${clause}; the native review is an additional outside view that never replaces verification, so the risk-gated path applies exactly as receipt-driven development off, and`,
446
451
  risk,
447
- writerProfile,
448
452
  );
449
453
  }
450
454
 
451
455
  // rddLine is "off" or "unknown" here -- both gate on the native risk
452
456
  // assessment identically, so an unknown RDD line never lowers a tier, and
453
457
  // neither line ever consults nativeReviewOutcome.
454
- return riskGatedPlan(`receipt-driven development is ${rddLine} and`, risk, writerProfile);
458
+ return riskGatedPlan(`receipt-driven development is ${rddLine} and`, risk);
455
459
  }
456
460
 
457
461
  // ---------------------------------------------------------------------------
@@ -476,15 +480,16 @@ export function verificationPlan(input )
476
480
  const SMALL_MODEL_ID_TOKEN = /(?:^|[-_./:\s])mini(?:$|[-_./:\s])/i;
477
481
 
478
482
  /**
479
- * A writer profile counts as a "small model" for the medium-risk bias when:
483
+ * A writer profile counts as a "small model" (reported by ASSESS as
484
+ * `writerProfile`; it never changes the verification plan) when:
480
485
  * - its resolved effort is `low`, or
481
486
  * - its resolved model id carries `mini` as a whole token (see
482
487
  * `SMALL_MODEL_ID_TOKEN`), or
483
488
  * - it carries NO signal at all -- no profile object, or a profile with
484
489
  * neither a model id nor a resolved effort. An unknown or omitted writer
485
- * profile fails closed to small rather than defaulting to large, so a
486
- * caller that cannot resolve the writer's profile never gets a weaker
487
- * verification plan than a known small model would (gentle-pi#662).
490
+ * profile fails closed to small rather than defaulting to large. The
491
+ * result is reported in the assessment only: the verification plan never
492
+ * reads the writer profile, so it cannot add or remove a verifier.
488
493
  *
489
494
  * A profile that carries a model id or an effort, just not a matching one
490
495
  * (for example `{model: {id: "claude-sonnet-5"}, thinking: "high"}`), is
@@ -502,9 +507,9 @@ export function isSmallWriterProfile(profile
502
507
  }
503
508
 
504
509
  /**
505
- * Maps a resolved writer profile to the `writerProfile` input `verificationPlan`
506
- * expects. An unknown or omitted profile never lowers a tier: it resolves to
507
- * `"small"`, exactly like `isSmallWriterProfile` (gentle-pi#662).
510
+ * Maps a resolved writer profile to the reported `writerProfile`. An unknown
511
+ * or omitted profile resolves to `"small"`, exactly like
512
+ * `isSmallWriterProfile` (gentle-pi#662).
508
513
  */
509
514
  export function resolveWriterProfile(profile ) {
510
515
  return isSmallWriterProfile(profile) ? WRITER_PROFILE.SMALL : WRITER_PROFILE.LARGE;
@@ -22,6 +22,7 @@ const sources = [
22
22
  "gentle-shell-startup-flow",
23
23
  "gentle-shell-resume-hint",
24
24
  "gentle-shell-uninstall",
25
+ "gentle-shell-import-pi",
25
26
  ];
26
27
  const header = "// Generated by scripts/build-runtime-modules.mjs. Do not edit.\n";
27
28
 
@@ -0,0 +1,119 @@
1
+ // Explicit bundled-store recovery. The provider owns catalog validation and
2
+ // migration; Shell owns promotion and its core-bound provenance. Never called
3
+ // from ordinary startup or upgrade.
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import { closeSync, fsyncSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
6
+ import { dirname, join } from "node:path";
7
+ import { tmpdir } from "node:os";
8
+ import { spawnSync } from "node:child_process";
9
+ import { engramProbeAllowed, engramProvenancePath, parseEngramProvenance } from "../runtime/gentle-shell-bundled.mjs";
10
+
11
+ export const MIGRATION_USAGE = "Usage: gentle-shell memory migrate --confirm-quiescent\nStop every Engram client before running this command.\n";
12
+ export const MIGRATION_PENDING_NOTICE = "Engram memory disabled: an incomplete memory migration needs recovery. Keep every Engram client stopped, retain the original backup, and follow docs/bundled-install.md#recovering-an-interrupted-migration. Do not remove the migration marker or create a new store before recovery.";
13
+ export const migrationMarkerPath = (prefix) => join(prefix, "config", "engram-migration.json");
14
+ export function migrationPending(prefix) {
15
+ try { lstatSync(migrationMarkerPath(prefix)); return true; }
16
+ catch (error) { return error.code !== "ENOENT"; }
17
+ }
18
+ function regularText(path, limit) {
19
+ const info = lstatSync(path);
20
+ if (!info.isFile() || info.size > limit) throw new Error("unsafe migration metadata");
21
+ return readFileSync(path, "utf8");
22
+ }
23
+ const digest = (path) => createHash("sha256").update(readFileSync(path)).digest("hex");
24
+ // Durably record a file or a directory entry change. Windows cannot open a
25
+ // directory for fsync; there NTFS metadata journaling covers the renames.
26
+ function syncPath(path, directory = false) {
27
+ if (directory && process.platform === "win32") return;
28
+ const fd = openSync(path, "r");
29
+ try { fsyncSync(fd); } finally { closeSync(fd); }
30
+ }
31
+ function coreHelp(core, env) {
32
+ // Older cores route unknown commands through legacy startup. Inspect their
33
+ // config-free help in an isolated home, never try an unknown data command.
34
+ const scratch = mkdtempSync(join(tmpdir(), "gentle-engram-capability-"));
35
+ try {
36
+ const probe = spawnSync(core, ["--help"], {
37
+ cwd: scratch, env: { ...env, HOME: scratch, USERPROFILE: scratch, ENGRAM_DATA_DIR: join(scratch, "store") },
38
+ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 5000, maxBuffer: 65536, windowsHide: true,
39
+ });
40
+ return !probe.error && !probe.signal && probe.status === 0 && probe.stderr === "" ? probe.stdout.split("\n").map((line) => line.trim()) : [];
41
+ } finally { rmSync(scratch, { recursive: true, force: true }); }
42
+ }
43
+ function supportsMigration(core, env) {
44
+ return coreHelp(core, env).includes("migrate-copy --data-dir <absolute-source> --output-dir <new-absolute-destination> --confirm-quiescent --json");
45
+ }
46
+
47
+ export function migrateBundledEngram({ prefix, core, home, env }) {
48
+ let markerOwned = false;
49
+ let promotionStarted = false;
50
+ let output;
51
+ let backup;
52
+ try {
53
+ if (!core) throw new Error("the verified bundled core is unavailable; run gentle-shell setup first");
54
+ if (!engramProbeAllowed(env)) throw new Error("explicit Engram selections or a disabled startup policy must be resolved before bundled migration");
55
+ if (migrationPending(prefix)) throw new Error(MIGRATION_PENDING_NOTICE);
56
+ const source = join(realpathSync(home), ".engram");
57
+ if (!lstatSync(source).isDirectory() || realpathSync(source) !== source) throw new Error("the source must be a regular default store directory");
58
+ const provenancePath = engramProvenancePath(prefix);
59
+ const original = regularText(provenancePath, 4096);
60
+ const record = parseEngramProvenance(original);
61
+ const identity = regularText(join(source, ".instance-id"), 128).trim();
62
+ if (!record || record.dataDir !== source || record.instanceId !== identity) throw new Error("the store does not match Shell's recorded identity; it cannot be adopted");
63
+ const coreHash = digest(core);
64
+ const childEnv = Object.fromEntries(Object.entries(env).filter(([key]) => !key.startsWith("ENGRAM_") && key !== "GH_TOKEN" && key !== "GITHUB_TOKEN"));
65
+ childEnv.ENGRAM_NO_UPDATE_CHECK = "1";
66
+ if (!supportsMigration(core, childEnv)) throw new Error("the bundled core does not support migrate-copy; wait for a supporting published bundle. The source and provenance are unchanged");
67
+ const suffix = randomUUID();
68
+ output = `${source}.migrating-${suffix}`;
69
+ backup = `${source}.before-${suffix}`;
70
+ const marker = migrationMarkerPath(prefix);
71
+ mkdirSync(dirname(marker), { recursive: true, mode: 0o700 });
72
+ // A recorded refusal precedes copying/promotion. A killed launcher must not
73
+ // mistake the rename gap for a fresh install and provision an empty store.
74
+ writeFileSync(marker, `${JSON.stringify({ source, output, backup, originalProvenance: record })}\n`, { flag: "wx", mode: 0o600 });
75
+ markerOwned = true;
76
+ syncPath(marker);
77
+ syncPath(dirname(marker), true);
78
+ const result = spawnSync(core, ["migrate-copy", "--data-dir", source, "--output-dir", output, "--confirm-quiescent", "--json"], {
79
+ env: childEnv, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 120000, maxBuffer: 16384, windowsHide: true,
80
+ });
81
+ if (result.error || result.signal) throw new Error("the provider failed or timed out; no source promotion was attempted");
82
+ if (result.status !== 0) throw new Error(`the provider rejected migration (exit ${result.status}): ${result.stderr.trim().slice(0, 2048) || "no diagnostic"}`);
83
+ if (result.stderr !== "") throw new Error("unexpected provider diagnostic with success evidence");
84
+ let proof;
85
+ try { proof = JSON.parse(result.stdout); } catch { throw new Error("invalid provider migration evidence"); }
86
+ if (!proof || proof.migration_completed !== true || proof.source_dir !== source || proof.output_dir !== output || proof.instance_id !== identity) {
87
+ throw new Error("provider migration evidence does not match the requested store and copy");
88
+ }
89
+ if (!lstatSync(output).isDirectory() || realpathSync(output) !== output ||
90
+ regularText(join(output, ".instance-id"), 128).trim() !== identity ||
91
+ regularText(join(source, ".instance-id"), 128).trim() !== identity ||
92
+ regularText(provenancePath, 4096) !== original || digest(core) !== coreHash) {
93
+ throw new Error("store, provenance or bundled core changed during migration");
94
+ }
95
+ // The original is retained, not deleted. From this point any failure keeps
96
+ // the marker: recovery must reconcile both paths before another launch.
97
+ promotionStarted = true;
98
+ renameSync(source, backup);
99
+ renameSync(output, source);
100
+ syncPath(dirname(source), true);
101
+ const temporary = `${provenancePath}.${suffix}.tmp`;
102
+ writeFileSync(temporary, `${JSON.stringify({ schema: 1, dataDir: source, instanceId: identity, coreSha256: coreHash })}\n`, { flag: "wx", mode: 0o600 });
103
+ syncPath(temporary);
104
+ renameSync(temporary, provenancePath);
105
+ syncPath(dirname(provenancePath), true);
106
+ // Only a durable promotion and provenance may release the fail-closed marker.
107
+ rmSync(marker);
108
+ // The migration is complete; a failed final sync must not report failure.
109
+ try { syncPath(dirname(marker), true); } catch { /* Best effort. */ }
110
+ return { code: 0, stdout: `${JSON.stringify({ status: "migrated", instance_id: identity, backup_dir: backup })}\n`, stderr: "" };
111
+ } catch (error) {
112
+ if (markerOwned && !promotionStarted) {
113
+ try { rmSync(migrationMarkerPath(prefix)); } catch { /* Keep the fail-closed marker. */ }
114
+ }
115
+ const retained = output ? ` Any partial copy at ${output} is retained; do not use it.` : "";
116
+ const recovery = promotionStarted ? ` Promotion may be incomplete; original backup: ${backup}. ${MIGRATION_PENDING_NOTICE}` : "";
117
+ return { code: 1, stdout: "", stderr: `gentle-shell: memory migration failed: ${error.message}.${retained}${recovery}\n` };
118
+ }
119
+ }
@@ -37,7 +37,7 @@ const WINDOWS_SYSTEM_ROOT = "C:\\Windows";
37
37
  // version check below) derives from this constant instead of repeating the
38
38
  // literal, so a pin bump cannot leave a stale copy behind. See
39
39
  // scripts/install-gentle-ai.mjs for the incident that motivated this.
40
- export const INSTALLER_VERSION = "4.1.1";
40
+ export const INSTALLER_VERSION = "4.3.1";
41
41
  export const RELEASE_BASE_URL = `https://github.com/Gentleman-Programming/gentle-ai/releases/download/v${INSTALLER_VERSION}/`;
42
42
  export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
43
43
  SIGNED_RELEASE_ASSET: "signed-release-asset",
@@ -46,11 +46,11 @@ export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
46
46
  export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH = "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai";
47
47
  export const GENTLE_AI_WINDOWS_SOURCE_MODULE = "github.com/gentleman-programming/gentle-ai/v4";
48
48
  export const GENTLE_AI_WINDOWS_SOURCE_TAG = `v${INSTALLER_VERSION}`;
49
- // `go mod download -json github.com/gentleman-programming/gentle-ai/v4@v4.1.1`
49
+ // `go mod download -json github.com/gentleman-programming/gentle-ai/v4@v4.3.1`
50
50
  // with GOSUMDB=sum.golang.org reports this exact module SumDB checksum, and the
51
- // tag resolves to commit 898d3976e8049a7238eaa7de60bff325c30d2b3d, the published v4.1.1 release head.
51
+ // tag resolves to commit 2dba7784114fff57864519c4c0d067e952a8c98b, the published v4.3.1 release head.
52
52
  // v4.0.0 moved the Go module path to the /v4 major-version suffix.
53
- export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:+WmZ2IvIOiJ8qtHa7wsjnYKNasem+PByeOZ6HIjoAL8=";
53
+ export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:00p8dnjJrhYFq1Qikk2oY6K/UXIty9A/IBUUOUiX9XA=";
54
54
  export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE = `${GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH}@${GENTLE_AI_WINDOWS_SOURCE_TAG}`;
55
55
  export const GENTLE_AI_WINDOWS_MINIMUM_GO_VERSION = "1.25.10";
56
56
  export const GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE_CODE = "GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE";
@@ -69,7 +69,7 @@ export class GentleAiInstallerError extends Error {
69
69
  // Sentinel used while a re-pinned gentle-ai release is not yet published. A
70
70
  // sentinel digest can never match a real SHA-256, so installation fails closed,
71
71
  // and verify-package-files.mjs refuses to pack/publish while any digest below
72
- // still holds it. The v4.1.1 digests are pinned from the published release:
72
+ // still holds it. The v4.3.1 digests are pinned from the published release:
73
73
  // archive sha256 values verified against the minisign-signed checksums.txt and
74
74
  // freshly computed hashes; binary sha256 values computed from the extracted
75
75
  // executables.
@@ -111,15 +111,15 @@ async function downloadPinnedGentleAiAsset(asset, destination, options) {
111
111
  }
112
112
 
113
113
  // Windows is absent from signed release archives on purpose. gentle-ai stopped
114
- // distributing unsigned Windows builds in c4b764d0, so v4.1.1 publishes signed
114
+ // distributing unsigned Windows builds in c4b764d0, so v4.3.1 publishes signed
115
115
  // Darwin/Linux archives only. Windows x64/arm64 uses the separately verified
116
116
  // exact-tag Go SumDB source-build path below; restore archive rows only when
117
117
  // upstream ships signed Windows assets.
118
118
  export const GENTLE_AI_RELEASE_ASSETS = Object.freeze({
119
- "darwin/amd64": asset("gentle-ai_4.1.1_darwin_amd64.tar.gz", "f2e9aebfba3056e88a8d7c7193e4e8c04282dc126d5eaa2216bc807deadecffd", "82351dd86bf7323c6a5b9e9e68b3cc2893a65a4df04fbdf1ef1801cabf5d2338", "gentle-ai"),
120
- "darwin/arm64": asset("gentle-ai_4.1.1_darwin_arm64.tar.gz", "8093dced37130cb26534d81663013f81d9039342e33f00b8c85aa5319aa7eea9", "6362e3641b87ae2b41a8dd7695dad4cf47457ee86c9f63792e474476bfdb6b6a", "gentle-ai"),
121
- "linux/amd64": asset("gentle-ai_4.1.1_linux_amd64.tar.gz", "57869ebcdba5ba9942493ab413bbc15d146e2451c4dca3eaafcf163fcbd646b4", "7c638c8467efc7036e0ec6b8a1d9080a5e9a64f7ae6cbc6074fce577f789b7fe", "gentle-ai"),
122
- "linux/arm64": asset("gentle-ai_4.1.1_linux_arm64.tar.gz", "5f8fa38573eea0f2110902f0996aa0b008aac58dc0f9a6abbdf301dcbc93f4eb", "76a36a73d6905a16221496ffe9a193bd79e17951463d711cd03abf370349ef55", "gentle-ai"),
119
+ "darwin/amd64": asset("gentle-ai_4.3.1_darwin_amd64.tar.gz", "6cf57fee0574daef2abed07f54a355fec3b93ac8f01733dc6c4d3674f1746112", "ca44c3fce186b63a7e7ad455c338133ba1982f4a13485301ce4d66edb73d2928", "gentle-ai"),
120
+ "darwin/arm64": asset("gentle-ai_4.3.1_darwin_arm64.tar.gz", "13e8828447f9081465ee0c35a61b24fb49296d51a310c6999227e2aed178956b", "749d08062b0dec13681678a1a1c018613d223ddcca47b5f27681b1d8ab5d14c2", "gentle-ai"),
121
+ "linux/amd64": asset("gentle-ai_4.3.1_linux_amd64.tar.gz", "04ec685c56b42c29dff6db10fc7dc2c730217dc9c96736e53ad2acf3226dafb7", "86248792bde3fe5e9ca93b128c07e7caa6a5b31fac0f9fc84b5b3011b123cb90", "gentle-ai"),
122
+ "linux/arm64": asset("gentle-ai_4.3.1_linux_arm64.tar.gz", "99e7d313a2ff85eaf23a6415e9b3aae67bf74c2fa169b21b93070ada7613f6a0", "df3580506bb5ea63cc4ef5ccebe8b68cfedf66b72f3b51f84ff393183d8cea2b", "gentle-ai"),
123
123
  });
124
124
 
125
125
  // A pinned asset is either a signed archive or, for a prerelease pin only,
@@ -44,15 +44,15 @@ export const goPinVersion = go.version;
44
44
  // checksums.txt and GitHub's asset digests; every archive was downloaded and
45
45
  // re-hashed before pinning. GitHub serves the assets from its asset hosts.
46
46
  const engram = Object.freeze({
47
- version: "3.4.0",
47
+ version: "3.5.2",
48
48
  redirectHosts: Object.freeze(["objects.githubusercontent.com", "release-assets.githubusercontent.com"]),
49
49
  archives: Object.freeze({
50
- "darwin-arm64": ["darwin_arm64.tar.gz", "d0aa59c9a83d17ae3fc7f5dd31fdab6a26fd8c00fc5e692195753cd0719294f7", 7470202],
51
- "darwin-x64": ["darwin_amd64.tar.gz", "3c5bdc73d226af5629cf4f4a781e223142e37c1ebb443c4b596f2e284352e381", 7961674],
52
- "linux-x64": ["linux_amd64.tar.gz", "715fda625556e46d65ddb4526bb7f88a3fee5339ca3c44ce44cc652f17fa57f1", 7821191],
53
- "linux-arm64": ["linux_arm64.tar.gz", "3309e9c0bbb539d183fd5aa9f9a13cd7aa0e81ec2512b5bedefb8b2bdd35b3e5", 7176726],
54
- "win32-x64": ["windows_amd64.zip", "3065736ec7ff8137eb3fc607275f542ea13bb743a5157318c8a8770f11ce3289", 8021225],
55
- "win32-arm64": ["windows_arm64.zip", "7463872a85a5e5434cf958bdffc71dda1926bced30d3b6b38c80fec9bb98fbba", 7231641],
50
+ "darwin-arm64": ["darwin_arm64.tar.gz", "43d398b2fe3327a4a6436c26b2a17d3285ba80bf2f11a22d0796ea2a7fd2b78c", 7749520],
51
+ "darwin-x64": ["darwin_amd64.tar.gz", "aea28ebf461a4ea86c52b793c29311539d7123875354ed7411eae982fc82ec87", 8181300],
52
+ "linux-x64": ["linux_amd64.tar.gz", "106a43ca8f43a27c2c260a6787151c3be6d91c5ed3d5e454e6140527adf92e01", 8064598],
53
+ "linux-arm64": ["linux_arm64.tar.gz", "b0ba49cf5a7d5af436c9744a9a0d638b3f9ebb2a7321c8736d30e96305c2993c", 7472279],
54
+ "win32-x64": ["windows_amd64.zip", "ba56a36d37b2da85fcb1581bbdbed9ae1ee690e642aab8dbe1e18209bf264afe", 8272809],
55
+ "win32-arm64": ["windows_arm64.zip", "df5acc5dd6ee407d484074e570c77a79567c0b8dccf55e25ae25a644c1e7db5f", 7530282],
56
56
  }),
57
57
  });
58
58
  /** The Engram version the bundled install publishes under runtime/. */
@@ -248,9 +248,11 @@ export function planPreflight(inventory, { channel = "release" } = {}) {
248
248
  record("gentleAi", classify(inventory.gentleAi, requirements.gentleAi, (o) => o.compatible, true), requirements.gentleAi);
249
249
  const needsNative = tools.gentleAi.status === "unavailable";
250
250
  // The main channel builds Gentle AI from source on every platform.
251
- // gentle-pi's postinstall may build Gentle AI from source on Windows, so a
252
- // Windows release update needs Go like an installation; main always does.
253
- const windowsBuild = platform === "win32" && (update !== null || (needsNative && !npmCurrent));
251
+ // gentle-pi's postinstall always builds Gentle AI from source on Windows, even
252
+ // next to an installed gentle-ai, so installing or updating the Shell needs Go,
253
+ // as does provisioning a missing native binary; main always does.
254
+ const installsShell = tools.shell.status === "unavailable";
255
+ const windowsBuild = platform === "win32" && (update !== null || (!npmCurrent && (installsShell || needsNative)));
254
256
  const goStatus = windowsBuild || main ? classify(inventory.go, requirements.go) : "not-required";
255
257
  if (goStatus === "unavailable" || goStatus === "incompatible" || goStatus === "unknown") {
256
258
  // A missing, older or unreadable Go is left as it is: the build gets the
@@ -170,6 +170,9 @@ export const guidance = Object.freeze({
170
170
  setupRateLimit: "`gentle-shell setup` could not finish because GitHub's limit for anonymous API requests was reached on this network. Wait up to an hour, then run the installer again.",
171
171
  // A failed acquire-go whose detail names a Go folder the installer did not publish.
172
172
  goDestinationConflict: `A Go folder from an earlier, interrupted installer run is in the way, and the installer never replaces a folder it cannot prove it published. Remove that folder (shown below), then run the installer again. Nothing was installed.`,
173
+ // A failed install whose detail shows Windows refused to start the gentle-ai.exe gentle-pi's postinstall built.
174
+ codeIntegrityBlocked: "Windows refused to run the Gentle AI program (gentle-ai.exe) that the installation builds on this computer, most likely because Smart App Control or another code integrity policy blocks unsigned programs. " +
175
+ "Windows offers no exception for a single program. Keep using Gentle Shell in WSL, or turn Smart App Control off in Windows Security and run the installer again; on Windows 11 it cannot be turned back on without reinstalling Windows.",
173
176
  // A failed persist-path whose detail shows pnpm could not tell the shell (POSIX SHELL missing or unsupported).
174
177
  persistPathShell: "`pnpm setup` could not tell which shell profile to edit because the SHELL environment variable is missing or names an unsupported shell. Open a regular terminal and run the installer again, or add `$PNPM_HOME/bin` to your PATH yourself.",
175
178
  fallback: "The installation stopped for an unexpected reason. Nothing else will run; check the terminal and run the installer again.",
@@ -186,16 +189,20 @@ function setupDetail(value) {
186
189
  const rateLimited = (detail) => /rate limit/i.test(detail) || (/GitHub API/i.test(detail) && /\b403\b/.test(detail));
187
190
  const unknownShell = (detail) => /ERR_PNPM_(?:UNKNOWN|UNSUPPORTED)_SHELL/.test(detail);
188
191
  const goConflict = (detail) => detail.startsWith("Conflicting Go destination: ");
192
+ // libuv reports a CreateProcess refusal it has no code for, such as a Windows code integrity block
193
+ // (Smart App Control, event 3077), as spawn UNKNOWN.
194
+ const codeIntegrityBlocked = (detail) => /\bspawn UNKNOWN\b/.test(detail);
189
195
  // The main channel's typed error when GitHub refused the latest main commit with HTTP 403.
190
196
  const githubLimited = (detail) => rateLimited(detail) || (/\bmain-commit-unavailable\b/.test(detail) && /\bHTTP 403\b/.test(detail));
191
197
  // Fixed steps whose sanitized last error line may reach the browser.
192
198
  // The bundled steps send a detail only for a migration (its error message); a new install's send none.
193
199
  const bundledDetail = Object.fromEntries(["claim-prefix", "copy-npm-settings", "install-runtime", "activate-version", "write-launcher", "verify-launcher", "bundled-path"]
194
200
  .map((step) => [step, () => false]));
195
- const detailSteps = Object.freeze({ ...bundledDetail, "shell-setup": rateLimited, "install-version": () => false, "persist-path": unknownShell, "acquire-go": goConflict,
196
- "install-global": githubLimited, "install-shell-main": githubLimited, "build-gentle-ai-main": githubLimited, "update-shell": githubLimited });
201
+ const detailSteps = Object.freeze({ ...bundledDetail, "shell-setup": rateLimited, "install-version": codeIntegrityBlocked, "persist-path": unknownShell, "acquire-go": goConflict,
202
+ "install-global": (detail) => githubLimited(detail) || codeIntegrityBlocked(detail), "install-shell-main": githubLimited, "build-gentle-ai-main": githubLimited, "update-shell": githubLimited });
203
+ // A step whose detail can match more than one pattern names its guidance by detail.
197
204
  const detailGuidance = Object.freeze({ "shell-setup": "setupRateLimit", "persist-path": "persistPathShell", "acquire-go": "goDestinationConflict",
198
- "install-global": "githubRateLimit", "install-shell-main": "githubRateLimit", "build-gentle-ai-main": "githubRateLimit", "update-shell": "githubRateLimit" });
205
+ "install-version": "codeIntegrityBlocked", "install-global": (detail) => (githubLimited(detail) ? "githubRateLimit" : "codeIntegrityBlocked"), "install-shell-main": "githubRateLimit", "build-gentle-ai-main": "githubRateLimit", "update-shell": "githubRateLimit" });
199
206
 
200
207
  const ID = /^[a-z][a-z0-9-]{0,63}$/;
201
208
  function identifier(value, fallback = "unknown") {
@@ -543,7 +550,10 @@ function outcomeView(result) {
543
550
  const detail = step ? setupDetail(result.detail) : null;
544
551
  if (detail) {
545
552
  view.detail = detail;
546
- if (detailSteps[step](detail)) view.guidance = guidance[detailGuidance[step]];
553
+ if (detailSteps[step](detail)) {
554
+ const key = detailGuidance[step];
555
+ view.guidance = guidance[typeof key === "function" ? key(detail) : key];
556
+ }
547
557
  }
548
558
  } else {
549
559
  view.guidance = guidance.outcomes[result.outcome];