gentle-pi 4.0.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (425) hide show
  1. package/README.md +157 -18
  2. package/assets/agents/gentle-ai-explore.md +11 -4
  3. package/assets/agents/gentle-ai-verify.md +23 -4
  4. package/assets/agents/gentle-ai-worker.md +6 -6
  5. package/assets/install-wizard/index.html +47 -0
  6. package/assets/install-wizard/wizard.css +1279 -0
  7. package/assets/install-wizard/wizard.js +951 -0
  8. package/assets/orchestrator-delegation.md +70 -163
  9. package/assets/orchestrator-memory.md +11 -4
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-tracking.md +53 -0
  12. package/assets/orchestrator-verification.md +28 -0
  13. package/assets/orchestrator-writer.md +38 -0
  14. package/assets/orchestrator.md +29 -25
  15. package/assets/sounds/LICENSE.md +52 -0
  16. package/assets/sounds/attention.wav +0 -0
  17. package/assets/sounds/error.wav +0 -0
  18. package/assets/sounds/success.wav +0 -0
  19. package/bin/engram-guard +9 -0
  20. package/bin/gentle-shell-install.mjs +302 -0
  21. package/bin/gentle-shell.mjs +708 -22
  22. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  23. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  24. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  25. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  27. package/docs/auto-permission-answers.md +25 -0
  28. package/docs/auto-permission-evidence.md +39 -0
  29. package/docs/bundled-install.md +399 -0
  30. package/docs/delegated-verification.md +3 -5
  31. package/docs/gentle-agents-activity.md +513 -0
  32. package/docs/gentle-shell-uninstall.md +136 -0
  33. package/docs/gentle-shell.md +90 -33
  34. package/docs/install-wizard.md +2040 -0
  35. package/docs/native-authority-architecture.md +1 -1
  36. package/docs/permission-modes.md +127 -0
  37. package/docs/prompt-history.md +1 -1
  38. package/docs/readme-reference.md +310 -47
  39. package/docs/render-cache-performance.md +28 -0
  40. package/docs/session-profile-format.md +39 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/yolo-mode.md +11 -10
  44. package/extensions/ask-user-choice.ts +4 -0
  45. package/extensions/ask-user-question.ts +6 -1
  46. package/extensions/child-context.ts +11 -2
  47. package/extensions/child-safety.ts +4 -2
  48. package/extensions/codegraph-tools.ts +18 -5
  49. package/extensions/gentle-agents.ts +718 -87
  50. package/extensions/gentle-ai.ts +991 -263
  51. package/extensions/gentle-herdr-activity.ts +81 -0
  52. package/extensions/gentle-notifications.ts +104 -0
  53. package/extensions/gentle-shell.ts +271 -80
  54. package/extensions/gentle-todo.ts +123 -18
  55. package/extensions/history/index.ts +11 -6
  56. package/extensions/pi-pretty.ts +39 -3
  57. package/extensions/quiet-tools.ts +91 -21
  58. package/extensions/skill-registry.ts +175 -46
  59. package/extensions/startup-banner.ts +11 -2
  60. package/lib/agent-home.ts +10 -1
  61. package/lib/agent-model-resolution.ts +80 -0
  62. package/lib/agent-profiles.ts +13 -3
  63. package/lib/agents-completion-delivery.ts +11 -3
  64. package/lib/agents-messaging.ts +1 -1
  65. package/lib/agents-protocol.ts +40 -1
  66. package/lib/agents-runner.ts +90 -15
  67. package/lib/auto-permission-answers.ts +133 -0
  68. package/lib/auto-permission-classifier.ts +459 -0
  69. package/lib/auto-permission-evidence.ts +88 -0
  70. package/lib/auto-permission-memory.ts +69 -0
  71. package/lib/auto-permission-runtime.ts +484 -0
  72. package/lib/auto-permission-tool-context.ts +61 -0
  73. package/lib/background-jobs-tools.ts +202 -0
  74. package/lib/background-jobs.ts +253 -0
  75. package/lib/background-monitor.ts +154 -0
  76. package/lib/bounded-writer-admission.ts +83 -10
  77. package/lib/builtin-codemode-optout.ts +166 -0
  78. package/lib/card-content-policy.ts +85 -0
  79. package/lib/child-package-injection.ts +62 -0
  80. package/lib/codemode-renderer.ts +14 -4
  81. package/lib/command-palette-catalog.ts +3 -2
  82. package/lib/context-threshold.ts +26 -0
  83. package/lib/destructive-command-guard.ts +104 -28
  84. package/lib/gentle-ai-binary.ts +109 -4
  85. package/lib/gentle-shell-bundled.ts +535 -0
  86. package/lib/gentle-shell-launcher.ts +75 -24
  87. package/lib/gentle-shell-startup-flow.ts +54 -0
  88. package/lib/gentle-shell-startup-update.ts +92 -0
  89. package/lib/gentle-shell-uninstall.ts +840 -0
  90. package/lib/gentle-shell-update-panel.ts +231 -0
  91. package/lib/herdr-activity.ts +183 -0
  92. package/lib/inprocess-reviewer.ts +3 -0
  93. package/lib/jobs-sidebar-state.ts +20 -0
  94. package/lib/jobs-view.ts +161 -0
  95. package/lib/model-price-ratio.ts +83 -0
  96. package/lib/model-routing-authority.ts +6 -1
  97. package/lib/nan-provider.ts +33 -11
  98. package/lib/native-review-cli.ts +194 -73
  99. package/lib/notification-audio-native.ts +162 -0
  100. package/lib/notification-audio-windows.ts +399 -0
  101. package/lib/notification-audio.ts +332 -0
  102. package/lib/notification-customize.ts +241 -0
  103. package/lib/notification-events.ts +113 -0
  104. package/lib/notification-policy.ts +148 -0
  105. package/lib/notification-pulse-client.ts +367 -0
  106. package/lib/notification-pulse-protocol.ts +305 -0
  107. package/lib/notification-pulse-stream.ts +213 -0
  108. package/lib/notification-pulse-worker.ts +66 -0
  109. package/lib/notification-scheduler.ts +173 -0
  110. package/lib/notification-service.ts +202 -0
  111. package/lib/notification-ui.ts +87 -0
  112. package/lib/odd-phase.ts +1 -1
  113. package/lib/orchestrator-alias.ts +20 -0
  114. package/lib/orchestrator-catalog.ts +79 -0
  115. package/lib/orchestrator-consultation.ts +61 -0
  116. package/lib/orchestrator-discovery.ts +49 -0
  117. package/lib/orchestrator-helper-consent.ts +122 -0
  118. package/lib/orchestrator-helper.ts +167 -0
  119. package/lib/orchestrator-message-card.ts +105 -0
  120. package/lib/orchestrator-presence.ts +115 -14
  121. package/lib/orchestrator-scope.ts +80 -0
  122. package/lib/orchestrator-state.ts +107 -0
  123. package/lib/orchestrator-tool-card.ts +280 -0
  124. package/lib/orchestrator-work-search.ts +218 -0
  125. package/lib/orchestrator-work.ts +79 -0
  126. package/lib/permission-mode-policy.ts +59 -0
  127. package/lib/review-candidate-view-owner.ts +8 -6
  128. package/lib/review-candidate-view.ts +3 -2
  129. package/lib/review-compact-contract.ts +2 -2
  130. package/lib/review-host-relay.ts +7 -2
  131. package/lib/review-integration-v2.ts +100 -22
  132. package/lib/review-publication-gate.ts +1 -1
  133. package/lib/review-risk-assessment.ts +77 -51
  134. package/lib/review-sidebar-state.ts +2 -1
  135. package/lib/review-transaction.ts +72 -3
  136. package/lib/runtime-metrics-children.ts +1 -1
  137. package/lib/runtime-metrics-native.ts +3 -2
  138. package/lib/selection-engine.ts +22 -7
  139. package/lib/session-change-capture.ts +5 -4
  140. package/lib/session-changes.ts +27 -22
  141. package/lib/session-profile-binding.ts +83 -0
  142. package/lib/session-profile-freeze.ts +261 -0
  143. package/lib/session-profile-persistence.ts +187 -0
  144. package/lib/session-worktree-registry.ts +74 -8
  145. package/lib/shell-bar.ts +41 -11
  146. package/lib/shell-changes-view.ts +40 -13
  147. package/lib/shell-changes.ts +88 -71
  148. package/lib/shell-prompt.ts +6 -4
  149. package/lib/shell-sidebar-layout.ts +5 -1
  150. package/lib/shell-todo.ts +81 -26
  151. package/lib/shell-usage.ts +4 -3
  152. package/lib/status-timing-diagnostics.ts +201 -0
  153. package/lib/telemetry-trigger.ts +3 -1
  154. package/lib/vim-editor-adapter.ts +27 -5
  155. package/lib/visual-customization-policy.ts +20 -9
  156. package/lib/visual-customize-view.ts +171 -20
  157. package/lib/writer-surfaces.ts +162 -0
  158. package/lib/yolo-session-policy.ts +198 -121
  159. package/package.json +5 -5
  160. package/runtime/child-package-injection.mjs +63 -0
  161. package/runtime/gentle-ai-binary.mjs +109 -4
  162. package/runtime/gentle-shell-bundled.mjs +536 -0
  163. package/runtime/gentle-shell-launcher.mjs +74 -23
  164. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  165. package/runtime/gentle-shell-startup-update.mjs +93 -0
  166. package/runtime/gentle-shell-uninstall.mjs +841 -0
  167. package/runtime/gentle-shell-update-panel.mjs +232 -0
  168. package/runtime/native-review-cli.mjs +191 -70
  169. package/runtime/review-integration-v2.mjs +100 -22
  170. package/runtime/review-risk-assessment.mjs +77 -51
  171. package/runtime/status-timing-diagnostics.mjs +202 -0
  172. package/runtime/telemetry-trigger.mjs +3 -1
  173. package/scripts/bootstrap.cmd +255 -0
  174. package/scripts/bootstrap.sh +168 -0
  175. package/scripts/build-distribution.mjs +157 -0
  176. package/scripts/build-installer-bundles.mjs +136 -0
  177. package/scripts/build-runtime-modules.mjs +7 -0
  178. package/scripts/bundled-install.mjs +1308 -0
  179. package/scripts/bundled-wizard.mjs +399 -0
  180. package/scripts/check-types.mjs +4 -6
  181. package/scripts/gentle-ai-installer.mjs +79 -32
  182. package/scripts/install-gentle-ai.mjs +48 -1
  183. package/scripts/install-tui-mode-setting.mjs +10 -1
  184. package/scripts/install-wizard-preview.mjs +154 -0
  185. package/scripts/installer-downloads.mjs +648 -0
  186. package/scripts/installer-preflight.mjs +387 -0
  187. package/scripts/installer-probes.mjs +564 -0
  188. package/scripts/installer-runner.mjs +1102 -0
  189. package/scripts/installer-server.mjs +881 -0
  190. package/scripts/installer-windows-artifacts.json +15 -0
  191. package/scripts/installer-windows.mjs +752 -0
  192. package/scripts/main-channel.mjs +452 -0
  193. package/scripts/npm-pack-result.mjs +31 -0
  194. package/scripts/pi-update.mjs +223 -0
  195. package/scripts/render-cache-performance-worker.mjs +205 -0
  196. package/scripts/render-cache-performance.mjs +115 -0
  197. package/scripts/run-test-suite.mjs +22 -8
  198. package/scripts/test-installer-acceptance.sh +161 -0
  199. package/scripts/test-packed-runner.mjs +23 -5
  200. package/scripts/types-baseline.json +2 -92
  201. package/scripts/verify-package-files.mjs +82 -18
  202. package/skills/gentle-ai/SKILL.md +12 -11
  203. package/tests/agent-home.test.ts +56 -3
  204. package/tests/agent-model-resolution.test.ts +113 -0
  205. package/tests/agent-profiles.test.ts +12 -0
  206. package/tests/agent-risk-escalation.test.ts +123 -0
  207. package/tests/agents-completion-delivery.test.ts +17 -0
  208. package/tests/agents-protocol.test.ts +57 -0
  209. package/tests/agents-queries.test.ts +9 -9
  210. package/tests/agents-runner-process.test.ts +31 -0
  211. package/tests/agents-runner.test.ts +89 -4
  212. package/tests/append-system-prompt-route.test.ts +18 -1
  213. package/tests/auto-mode-human-answers.test.ts +68 -0
  214. package/tests/auto-mode-invariants.test.ts +276 -0
  215. package/tests/auto-mode-sdk.test.ts +84 -0
  216. package/tests/auto-mode.test.ts +49 -0
  217. package/tests/auto-permission-answers.test.ts +170 -0
  218. package/tests/auto-permission-classifier.test.ts +634 -0
  219. package/tests/auto-permission-compound.test.ts +24 -0
  220. package/tests/auto-permission-evidence.test.ts +88 -0
  221. package/tests/auto-permission-memory.test.ts +46 -0
  222. package/tests/auto-permission-public-regressions.test.ts +111 -0
  223. package/tests/auto-permission-runtime.test.ts +454 -0
  224. package/tests/autonomous-guard.test.ts +86 -16
  225. package/tests/background-cache-warming.test.ts +2 -1
  226. package/tests/background-jobs.test.ts +227 -0
  227. package/tests/background-monitor.test.ts +195 -0
  228. package/tests/background-subagents-default-mode.test.ts +28 -5
  229. package/tests/bounded-stop-contract.test.ts +85 -0
  230. package/tests/bounded-writer-admission.test.ts +52 -1
  231. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  232. package/tests/bridge-wake-rendering.test.ts +56 -0
  233. package/tests/build-distribution.test.ts +269 -0
  234. package/tests/builtin-codemode-optout.test.ts +242 -0
  235. package/tests/bundled-install.test.ts +1319 -0
  236. package/tests/bundled-wizard.test.ts +505 -0
  237. package/tests/card-content-policy.test.ts +68 -0
  238. package/tests/card-style-policy.test.ts +34 -2
  239. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  240. package/tests/child-package-entrypoints.test.ts +65 -0
  241. package/tests/child-package-injection.test.ts +74 -0
  242. package/tests/child-safety.test.ts +12 -0
  243. package/tests/child-web-package.test.ts +224 -0
  244. package/tests/codegraph-tools.test.ts +35 -0
  245. package/tests/codemode-rendering.test.ts +107 -2
  246. package/tests/command-palette.test.ts +5 -5
  247. package/tests/context-threshold.test.ts +31 -0
  248. package/tests/delegate-for-reason-contract.test.ts +327 -0
  249. package/tests/delegated-key-learnings-contract.test.ts +3 -2
  250. package/tests/devbinary/native-review-parity.devtest.ts +1 -1
  251. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  252. package/tests/engram-safety.test.ts +316 -0
  253. package/tests/generic-agent-tools.test.ts +16 -1
  254. package/tests/gentle-agents.test.ts +1895 -76
  255. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  256. package/tests/gentle-ai-binary.test.ts +19 -2
  257. package/tests/gentle-ai-child-guards.test.ts +202 -0
  258. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  259. package/tests/gentle-ai-installer.test.ts +548 -58
  260. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  261. package/tests/gentle-ai-renderer.test.ts +6 -6
  262. package/tests/gentle-ai.test.ts +999 -31
  263. package/tests/gentle-herdr-activity.test.ts +100 -0
  264. package/tests/gentle-notifications.test.ts +427 -0
  265. package/tests/gentle-shell-bin.test.ts +263 -19
  266. package/tests/gentle-shell-bundled.test.ts +1036 -0
  267. package/tests/gentle-shell-launcher.test.ts +251 -25
  268. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  269. package/tests/gentle-shell-startup-update.test.ts +85 -0
  270. package/tests/gentle-shell-uninstall.test.ts +978 -0
  271. package/tests/gentle-shell-update-panel.test.ts +158 -0
  272. package/tests/gentle-shell.test.ts +611 -100
  273. package/tests/gentle-todo.test.ts +317 -14
  274. package/tests/herdr-activity.test.ts +243 -0
  275. package/tests/history-agent-home.test.ts +81 -0
  276. package/tests/history-child-guard.test.ts +70 -0
  277. package/tests/inprocess-reviewer.test.ts +20 -0
  278. package/tests/install-gentle-ai-failure.test.ts +69 -0
  279. package/tests/install-tui-mode-setting.test.ts +15 -0
  280. package/tests/install-wizard.test.ts +1223 -0
  281. package/tests/installer-bundles.test.ts +122 -0
  282. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  283. package/tests/installer-preflight.test.ts +750 -0
  284. package/tests/installer-probes.test.ts +1106 -0
  285. package/tests/installer-runner.test.ts +2159 -0
  286. package/tests/installer-server.test.ts +1567 -0
  287. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  288. package/tests/jobs-view.test.ts +160 -0
  289. package/tests/main-channel-upgrade.test.ts +395 -0
  290. package/tests/main-channel.test.ts +344 -0
  291. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  292. package/tests/model-price-ratio.test.ts +116 -0
  293. package/tests/model-profiles-preservation.test.ts +153 -0
  294. package/tests/model-routing-package.test.ts +10 -0
  295. package/tests/nan-provider-runtime.test.ts +156 -0
  296. package/tests/nan-provider.test.ts +28 -18
  297. package/tests/native-binary-gate.test.ts +2 -0
  298. package/tests/native-review-capability-contract.test.ts +7 -1
  299. package/tests/native-review-cli.test.ts +236 -1
  300. package/tests/native-review-consent.test.ts +2 -2
  301. package/tests/native-review-parity-runtime.test.ts +1 -1
  302. package/tests/native-review-parity.test.ts +12 -48
  303. package/tests/native-risk-evidence-parity.test.ts +21 -0
  304. package/tests/no-self-pause-contract.test.ts +87 -0
  305. package/tests/notification-audio-native.test.ts +281 -0
  306. package/tests/notification-audio-windows.test.ts +581 -0
  307. package/tests/notification-audio.test.ts +548 -0
  308. package/tests/notification-customize.test.ts +655 -0
  309. package/tests/notification-events.test.ts +115 -0
  310. package/tests/notification-policy.test.ts +201 -0
  311. package/tests/notification-pulse-client.test.ts +356 -0
  312. package/tests/notification-pulse-protocol.test.ts +213 -0
  313. package/tests/notification-pulse-stream.test.ts +320 -0
  314. package/tests/notification-scheduler.test.ts +258 -0
  315. package/tests/notification-ui.test.ts +157 -0
  316. package/tests/npm-pack-result.test.ts +41 -0
  317. package/tests/odd-integration.test.ts +14 -5
  318. package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
  319. package/tests/odd-routing-contract.test.ts +80 -23
  320. package/tests/odd-runtime-delegation-gate.test.ts +4 -1
  321. package/tests/orchestrator-budget.test.ts +29 -6
  322. package/tests/orchestrator-catalog.test.ts +146 -0
  323. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  324. package/tests/orchestrator-consultation.test.ts +111 -0
  325. package/tests/orchestrator-discovery.test.ts +241 -0
  326. package/tests/orchestrator-helper-consent.test.ts +171 -0
  327. package/tests/orchestrator-helper.test.ts +297 -0
  328. package/tests/orchestrator-message-card.test.ts +190 -0
  329. package/tests/orchestrator-presence.test.ts +23 -0
  330. package/tests/orchestrator-rdd-ownership.test.ts +6 -5
  331. package/tests/orchestrator-scope.test.ts +107 -0
  332. package/tests/orchestrator-state.test.ts +114 -0
  333. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  334. package/tests/orchestrator-tool-card.test.ts +398 -0
  335. package/tests/orchestrator-work-search.test.ts +318 -0
  336. package/tests/orchestrator-work.test.ts +147 -0
  337. package/tests/package-manifest.test.ts +105 -21
  338. package/tests/packed-runner-owned-path.test.ts +35 -1
  339. package/tests/parallel-reason-contract.test.ts +21 -0
  340. package/tests/parallel-writer-admission.test.ts +210 -0
  341. package/tests/permission-mode-policy.test.ts +194 -0
  342. package/tests/persona-single-channel.test.ts +4 -12
  343. package/tests/pi-pretty.test.ts +56 -0
  344. package/tests/pi-update-launcher.test.ts +87 -0
  345. package/tests/pi-update.test.ts +186 -0
  346. package/tests/provider-defect-handoff.test.ts +4 -3
  347. package/tests/quiet-tool-rendering.test.ts +160 -3
  348. package/tests/rdd-aware-verification-contract.test.ts +29 -41
  349. package/tests/rdd-status-line.test.ts +52 -0
  350. package/tests/render-cache-performance.test.ts +48 -0
  351. package/tests/review-agent-end-preflight.test.ts +62 -10
  352. package/tests/review-candidate-view.test.ts +101 -12
  353. package/tests/review-contract-prompt.test.ts +33 -4
  354. package/tests/review-controller-native-recovery.test.ts +327 -17
  355. package/tests/review-controller-native-routing.test.ts +255 -1
  356. package/tests/review-controller-workspace-root.test.ts +43 -2
  357. package/tests/review-controller.test.ts +2 -2
  358. package/tests/review-correction-lifecycle.test.ts +2 -1
  359. package/tests/review-gate.test.ts +7 -4
  360. package/tests/review-graph-schema.test.ts +2 -2
  361. package/tests/review-host-relay-routing.test.ts +5 -3
  362. package/tests/review-host-relay.test.ts +49 -2
  363. package/tests/review-integration-v2-forward.test.ts +21 -1
  364. package/tests/review-json-arguments.test.ts +167 -0
  365. package/tests/review-last-event-closure.test.ts +157 -8
  366. package/tests/review-ledger-contract.test.ts +5 -4
  367. package/tests/review-policy-judgment-day.test.ts +1 -1
  368. package/tests/review-policy-ordinary.test.ts +2 -2
  369. package/tests/review-relay-transport-agent.test.ts +66 -1
  370. package/tests/review-risk-assessment.test.ts +109 -33
  371. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  372. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  373. package/tests/review-test-fixtures.ts +7 -1
  374. package/tests/review-transaction.test.ts +18 -1
  375. package/tests/run-test-suite.test.ts +83 -5
  376. package/tests/runtime-harness.mjs +42 -15
  377. package/tests/runtime-metrics-extension.test.ts +1 -1
  378. package/tests/runtime-metrics-native.test.ts +31 -1
  379. package/tests/selection-engine.test.ts +65 -5
  380. package/tests/session-change-capture.test.ts +10 -10
  381. package/tests/session-changes-shell.test.ts +242 -48
  382. package/tests/session-changes.test.ts +17 -24
  383. package/tests/session-profile-binding.test.ts +163 -0
  384. package/tests/session-profile-freeze.test.ts +394 -0
  385. package/tests/session-profile-persistence.test.ts +334 -0
  386. package/tests/session-subject-routing-contract.test.ts +34 -0
  387. package/tests/session-worktree-registry.test.ts +57 -2
  388. package/tests/shell-bar.test.ts +40 -0
  389. package/tests/shell-changes-view.test.ts +85 -34
  390. package/tests/shell-changes.test.ts +79 -62
  391. package/tests/shell-render-cache-runtime.test.ts +69 -0
  392. package/tests/shell-render-cache.test.ts +129 -0
  393. package/tests/shell-sidebar-layout.test.ts +27 -2
  394. package/tests/shell-todo.test.ts +132 -11
  395. package/tests/shell-usage-view.test.ts +1 -1
  396. package/tests/shell-usage.test.ts +35 -0
  397. package/tests/short-check-routing-contract.test.ts +49 -0
  398. package/tests/skill-registry-watcher.test.ts +119 -0
  399. package/tests/skill-registry.test.ts +311 -5
  400. package/tests/startup-banner.test.ts +117 -3
  401. package/tests/startup-home-repo.test.ts +170 -0
  402. package/tests/status-timing-diagnostics.test.ts +283 -0
  403. package/tests/support/orchestrator-modules.ts +19 -0
  404. package/tests/task-size-routing-contract.test.ts +241 -0
  405. package/tests/telemetry-trigger.test.ts +27 -1
  406. package/tests/test-strength-contract.test.ts +124 -0
  407. package/tests/verify-agent-contract.test.ts +90 -0
  408. package/tests/verify-first-launch-contract.test.ts +139 -0
  409. package/tests/verify-package-files.test.ts +56 -0
  410. package/tests/vim-editor-adapter.test.ts +47 -21
  411. package/tests/visual-customization-policy.test.ts +32 -3
  412. package/tests/visual-customize-view.test.ts +288 -10
  413. package/tests/writer-edit-surface-scope.test.ts +59 -2
  414. package/tests/writer-surfaces.test.ts +199 -0
  415. package/tests/yolo-customize.test.ts +18 -3
  416. package/tests/yolo-mode-runtime.test.ts +37 -10
  417. package/tests/yolo-mode.test.ts +38 -5
  418. package/tests/yolo-session-policy.test.ts +1 -1
  419. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  420. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  421. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  422. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  423. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  424. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  425. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -0,0 +1,2040 @@
1
+ # Installation wizard groundwork
2
+
3
+ The preflight planner, real host probes, POSIX bootstrap, Windows prerequisite
4
+ foundation, the standard installation runner module (including runtime
5
+ persistence), the secure local wizard host with its packaged entry and the
6
+ interactive browser wizard UI are implemented. Windows native validation
7
+ remains unavailable locally.
8
+ **Preview:** download the unsigned double-click installer for your system
9
+ from a release, or run it from a checkout with `sh scripts/bootstrap.sh`
10
+ (macOS, Linux) or `scripts\bootstrap.cmd` (Windows), as the
11
+ [README](../README.md#easiest-download-and-double-click-no-terminal-needed)
12
+ shows. Native macOS/Windows clean-machine runs are still unverified (T7); one
13
+ Linux clean-container run passed (see
14
+ [Clean-machine acceptance](#clean-machine-acceptance-t7)).
15
+
16
+ ### Double-click downloads
17
+
18
+ [`scripts/build-installer-bundles.mjs`](../scripts/build-installer-bundles.mjs)
19
+ builds three archives with stable names, attached to every release by the
20
+ `installers` job of `publish.yml` after the verified npm publication (same
21
+ commit, `contents: write` only in that job), with
22
+ `gentle-shell-installers-SHA256SUMS.txt`:
23
+
24
+ | Asset | Launcher |
25
+ | --- | --- |
26
+ | `gentle-shell-installer-macos.zip` | `Gentle Shell Installer/Install Gentle Shell.command` |
27
+ | `gentle-shell-installer-windows.zip` | `Gentle Shell Installer/Install Gentle Shell.cmd` (CRLF, ends with `pause`) |
28
+ | `gentle-shell-installer-linux.tar.gz` | `gentle-shell-installer/install-gentle-shell.sh` |
29
+
30
+ Each launcher runs `installer/scripts/bootstrap.sh` (or `bootstrap.cmd`) next to
31
+ it; `installer/` holds `package.json`, every `installerPaths` file and every
32
+ module they import (only `node:` built-ins otherwise), as real files because the
33
+ bootstrap refuses symlinked bundle files. The downloads are **unsigned**:
34
+ macOS Gatekeeper and Windows SmartScreen warn on first run, and the README
35
+ explains how to continue. Signing and notarization are not done yet.
36
+
37
+ The `installers` job uploads to the release with the tag the `publish` job
38
+ verified (`needs.publish.outputs.tag`). It runs after npm publication, so its
39
+ failure never affects npm, but it marks the run failed: recover with
40
+ `gh run rerun <run id> --failed`, not a new `publish.yml` dispatch (that version
41
+ is already on npm). Each launcher reports whether the bootstrap succeeded before
42
+ it lets the window close.
43
+
44
+ ## What is available
45
+
46
+ `scripts/installer-preflight.mjs` exports four small integration surfaces:
47
+
48
+ | API | Contract |
49
+ | --- | --- |
50
+ | `requirements` | Repository-derived Node/Pi minima, pnpm acquisition pin, package version, native installer pin and Windows Go minimum. |
51
+ | `collectInventory({ platform, arch, probes })` | Calls injected named read-only probes serially. Missing/failed probes become unknown; error text is not retained. |
52
+ | `planPreflight(inventory)` | Pure classification and ordered action intents; no commands, downloads, installation or setup execution. |
53
+ | `pnpmGlobalBin({ platform, env })` | Pure pnpm 11 global-bin resolution: `{ pnpmHome, path, onPath }` or `null` when unknowable. |
54
+ | `persistencePins` | Fixed runtime persisted under PNPM_HOME: Node 24.21.0, its bundled npm 11.19.0 and the pnpm acquisition pin. |
55
+
56
+ `scripts/installer-probes.mjs` supplies the real read-only probes for
57
+ `collectInventory`; see [Host probes](#host-probes).
58
+
59
+ Current requirements come from `package.json` and
60
+ [`gentle-ai-installer.mjs`](../scripts/gentle-ai-installer.mjs): Node ≥22.19.0,
61
+ Pi ≥1.2.0, pnpm acquisition 11.1.1, Gentle Shell package 4.0.0 and package-local
62
+ Gentle AI 4.1.1. Windows native source builds require Go ≥1.25.10 when the
63
+ required usable native binary is missing. A reusable binary does not require Go.
64
+ When a build needs Go and the user's Go is missing, older or unchecked, the installer uses
65
+ its own [pinned Go](#pinned-go) instead of blocking.
66
+ Pins change with the repository; the API reads the metadata rather than copying
67
+ these values into a second installer.
68
+
69
+ ## Probe adapter contract
70
+
71
+ Supply `platform` as `linux`, `darwin` or `win32`, and `arch` as `x64` or `arm64`.
72
+ These are planning targets, **not evidence of native OS/libc support**. Other
73
+ pairs produce an unsupported-target blocker and no actions. Native lanes,
74
+ minimum OS versions, libc compatibility, artifact trust and elevation handling
75
+ remain to be verified in later units.
76
+
77
+ Probes are named `node`, `pnpm`, `pi`, `shell`, `gentleAi`, `go`, `globalBin`, and
78
+ `setup`, plus the optional advisory `git` (kept as `inventory.git` only when the
79
+ probe answers) and Windows `folders`. `collectInventory` has no default probes; the real implementations live
80
+ in [`installer-probes.mjs`](#host-probes). Probes must use bounded read-only
81
+ checks, never launcher setup, postinstall, paid model requests, credential
82
+ inspection or home writes. The collector cannot enforce purity of
83
+ caller-provided functions; adapter implementations require review.
84
+
85
+ - Tool observations use `{ available: true, version, usable: true }`; absence is
86
+ `{ available: false }`. Unknown availability, usability or unparseable versions
87
+ block planning. Versions are exact stable `major.minor.patch` strings (optional
88
+ `v` prefix); prereleases and raw command output are unknown, not silently reused.
89
+ - Node may add `persistent` (resolvable from the user's PATH without bootstrap
90
+ tool directories) and `npm` (a usable npm resolves there) booleans; pnpm
91
+ may add `persistent` (resolvable from the user's PATH). A `false` value
92
+ yields the runtime persistence intents below; absent or `null` values add
93
+ nothing, and the runner's own npm gate still applies.
94
+ - Shell additionally needs `global: true`, proving normal global ownership.
95
+ Compatible newer Node, Pi and Shell versions are reused, never downgraded.
96
+ - pnpm additionally needs `compatible: true`, based on its actual Node engine and
97
+ required global-install capabilities. The acquisition pin is not a minimum:
98
+ newer compatible pnpm is reusable. Do not infer compatibility from its version
99
+ alone; the later acquisition adapter must validate the pinned engine too.
100
+ - Gentle AI additionally needs `compatible: true`: evidence from the normal
101
+ package-local binary resolution/integrity contract, for the selected Shell
102
+ installation. Its exact installer pin is required; another version blocks
103
+ rather than being overwritten. Do not confuse an unrelated PATH binary with
104
+ the package's reusable native binary. For a newer reusable Shell, the adapter
105
+ must establish native-pin compatibility before presenting this evidence.
106
+ - Global-bin observations use `{ available: true, path, writable: true, onPath }`.
107
+ A known missing PATH entry yields an explicit setup intent. Unknown or
108
+ non-writable directories block; no directory is created or permission probed
109
+ by writing. pnpm 11 places global executables in **`$PNPM_HOME/bin`**, not
110
+ `$PNPM_HOME`, and `pnpm root -g`/`bin -g` fail when that directory is not on
111
+ PATH. Probes must report `pnpmGlobalBin().path`, whose `onPath` compares PATH
112
+ entries with that bin directory. PNPM_HOME is the user's existing absolute
113
+ value; otherwise pnpm's documented default (`$XDG_DATA_HOME/pnpm` or
114
+ `~/.local/share/pnpm` on Linux, `~/Library/pnpm` on macOS,
115
+ `%LOCALAPPDATA%\pnpm` on Windows). A relative PNPM_HOME or missing home is
116
+ unknown, not guessed. On Windows the wizard first checks that folder's
117
+ permissions and may use `%USERPROFILE%\.pnpm` instead or block
118
+ ([Windows PNPM_HOME](#windows-pnpm_home)).
119
+ - `setup` is a boolean evidence of normal Shell setup readiness. Unknown setup
120
+ on an existing stack blocks; a missing Shell/native binary needs normal setup.
121
+
122
+ ## Reading a plan
123
+
124
+ Tool statuses distinguish `unavailable`, `unknown`, `incompatible`, `reusable`,
125
+ `needs-setup`, `needs-update`, `needs-install` and `not-required`. A known version below the minimum is `incompatible` even when the tool cannot
126
+ run (for example an older Shell whose global command is missing); Gentle AI and
127
+ setup add no separate `unknown` blocker then, because they are only checked for
128
+ the pinned Shell. When Node.js, Pi, Gentle Shell or Go is simply older than its
129
+ minimum, the wizard's guidance names the found and required versions (for the
130
+ Shell, also the pnpm commands that update or remove it); other blockers keep
131
+ their fixed guidance. A missing, older or unchecked Go is not a blocker: `tools.go` is
132
+ `needs-acquire` with `required`, the pinned `version` and the older `found`
133
+ version (or `unchecked: true` when `go version` reported no version it can
134
+ check), and the plan acquires the [pinned Go](#pinned-go). An
135
+ older Node.js or an incompatible pnpm on the user's PATH is not a blocker when
136
+ the bootstrap's pinned copy runs the wizard: the probe reports that copy
137
+ (bootstrap-only) with the user's stable version as `found`, and `tools.node` or
138
+ `tools.pnpm` records `found` and the persisted pin as `version` (see
139
+ [Existing installations](#existing-installations)). That copy is persisted
140
+ only for a new installation of Gentle Shell: with an existing Shell (updated or
141
+ current) the replaced runtime is an `incompatible-tool` blocker before consent,
142
+ except for a setup recovery, which persists nothing. A user's pnpm in
143
+ `$PNPM_HOME/bin`, where persisting pnpm writes, is never reported as `found`:
144
+ the probe reports that pnpm itself with `inGlobalBin: true`, so an older or
145
+ newer-major one blocks and the guidance names both versions and, for an older
146
+ one, `pnpm self-update <pin>`. One whose shim is not recognized, or whose
147
+ `--version` does not report a stable version, is never run again: the probe
148
+ reports the bootstrap's pnpm in its place with `inGlobalBin: true`,
149
+ `unchecked: true` and `persistent: true` (on every platform), so it does not
150
+ block and nothing is persisted over it; `tools.pnpm` records `unchecked: true`
151
+ and the plan says before consent that it was left as it is and that the
152
+ installer uses its own verified pnpm. An older Pi is not a blocker: one that pnpm or npm owns is
153
+ `needs-update`, and one neither owns is `needs-install` (see
154
+ [Existing installations](#existing-installations)). A Pi or Shell command on PATH that pnpm does not manage
155
+ stays `unknown` (never replaced), and its probe adds `outsidePnpm: true` so the
156
+ guidance can say so; Gentle AI and setup add no separate blocker while the Shell
157
+ blocks. Any blocker suppresses all actions: repairing an
158
+ existing incompatible or uncertain component requires a later explicit decision,
159
+ not automatic replacement. `ready` means no acquisition/setup is indicated by
160
+ this inventory, **not that verification has executed**.
161
+
162
+ A clean target receives these intents in dependency order:
163
+
164
+ 1. Acquire and verify Node, then pnpm; prepare the usable global-bin environment.
165
+ 2. When a build needs Go (the main channel, or a Windows Gentle Shell install or
166
+ update whose postinstall may build Gentle AI) and Go is missing, older or unchecked,
167
+ `acquire-go` and `verify-go` come next, before every persistence and install
168
+ step, so a failed Go download leaves the computer as it was.
169
+ 3. Persist only what is missing. A bootstrap-only reusable Node gets the full
170
+ group, always together: `persist-node` (`persist-runtime`, version
171
+ 24.21.0), `persist-package-managers` (`install-global`) and
172
+ `configure-npm-prefix` (`configure`). When pnpm is already persistent
173
+ (`inventory.pnpm.persistent !== false`, such as a newer pnpm 11 in
174
+ `$PNPM_HOME/bin`), `persist-npm` replaces `persist-package-managers`, so
175
+ that pnpm is never replaced or downgraded. A persistent Node is never replaced:
176
+ it gets at most one intent, `persist-npm` (npm 11.19.0) when no usable
177
+ npm resolves (on POSIX a working npm from any version manager is usable), `persist-pnpm` (pnpm 11.1.1) when pnpm is bootstrap-only, or
178
+ `persist-package-managers` when both are missing.
179
+ 4. Install Pi globally, then `gentle-pi` globally (its existing postinstall owns
180
+ native installation). For an existing Shell with missing native binary, call
181
+ the existing installer instead.
182
+ 5. Run normal Shell setup and verify stack readiness. Verification is always
183
+ included, even when all components can be reused.
184
+
185
+ A recoverable setup (the pinned stack is installed, Gentle AI is verified and
186
+ only setup did not finish) plans exactly `setup-global-bin` (only when the
187
+ global bin is off PATH), `setup-shell` and `verify-readiness`: never an
188
+ acquisition, persistence or installation intent. See
189
+ [Setup recovery](#setup-recovery).
190
+
191
+ Actions are structured `{ id, kind, target, version? }` descriptors, **not shell
192
+ commands**. Later runners must re-inventory after changes, verify prerequisite
193
+ versions and global-bin readiness before dependent steps, obtain explicit consent
194
+ and stop on mandatory failures. They must preserve existing versions/paths and
195
+ use normal global package ownership and default `~/.gentle-shell/agent` semantics.
196
+ No alternate product root, Engram database/server, credential migration or custom
197
+ companion installer belongs in this plan.
198
+
199
+ ## Remaining boundaries
200
+
201
+ `npm:` registry sources are package references, not npm executable calls. Pi's
202
+ `npmCommand` supports pnpm, but independent upstream `npm exec` control remains
203
+ unverified. **This preflight does not establish an npm-free installation chain.**
204
+
205
+ The consent UI is the local wizard host described below. A published
206
+ distribution bundle and native acceptance evidence remain future work in the
207
+ [feature plan](../odd/tasks/browser-install-wizard.md). Deterministic injected
208
+ tests do not prove clean-machine installation on Windows, macOS or Linux.
209
+
210
+ Focused verification:
211
+
212
+ ```sh
213
+ node --experimental-strip-types --test tests/installer-server.test.ts tests/verify-package-files.test.ts tests/installer-probes.test.ts tests/installer-runner.test.ts tests/installer-preflight.test.ts tests/installer-posix-bootstrap.test.ts tests/installer-windows-bootstrap.test.ts
214
+ sh -n scripts/bootstrap.sh
215
+ ```
216
+
217
+ ## Host probes
218
+
219
+ `scripts/installer-probes.mjs` exports `createProbes({ platform, env, run, fs,
220
+ home?, verifyGentleAi? })`, returning the eight named probes for
221
+ `collectInventory`. `env` is the wizard's own environment, with bootstrap tools
222
+ first on PATH. Every probe returns the documented shape or the unknown shape
223
+ `{ available: null }`; adapter errors are caught and never retained.
224
+
225
+ | Export | Contract |
226
+ | --- | --- |
227
+ | `bootstrapRoots({ platform, env })` | Bootstrap tool roots: an absolute `GENTLE_BOOTSTRAP_TOOLS` plus every PATH entry segment named `.gentle-shell-bootstrap-tools.*` (case-insensitive on Windows). |
228
+ | `userEnvironment({ platform, env })` | A copy of `env` whose PATH omits entries inside those roots: the user's real PATH. |
229
+ | `hostAdapters({ maxOutputBytes?, maxTextBytes? })` | Real `run` and read-only `fs` adapters, described below. |
230
+
231
+ Adapters:
232
+
233
+ - `run(command, argv, { env, deadlineMs })` is the runner's process contract:
234
+ argv arrays with `shell:false`, SIGKILL at the deadline and stdout bounded
235
+ to `maxOutputBytes` (default 1 MiB, reported as `truncated: true`). stderr
236
+ is discarded because it can hold private paths, unless the caller passes
237
+ `stderrTail: <bytes>` (a positive integer, clamped to 4 KiB): then stderr is
238
+ piped and only its last bytes are kept and returned as `stderrTail`, also on a
239
+ deadline. Only the runner's `shell-setup`, `persist-path` and `pnpm add -g` steps
240
+ ask for it, and the main channel's `go install` and source extraction. A spawn failure returns
241
+ `code: null`. The deadline signals the direct child only, not descendants;
242
+ it then destroys the child's pipes and settles as timed out (`code: null`,
243
+ `signal: "SIGKILL"`) without waiting for `close`, which a descendant holding
244
+ stdout could otherwise delay indefinitely.
245
+ - `fs` offers `isFile`, `isDirectory`, `exists` (only ENOENT/ENOTDIR are
246
+ absent), `realpath`, `readText` (regular files up to `maxTextBytes`) and
247
+ `writable` (an access check, never a write). No probe creates, deletes or
248
+ writes anything.
249
+
250
+ Probes run only fixed argv: `node --version`, `pnpm --version`,
251
+ `pnpm list -g --depth 0 --json` (once, shared), `go version`, and
252
+ `npm --version` and `npm config get prefix` (POSIX), or npm's
253
+ `npm-cli.js --version` for a pinned or Windows npm. Deadlines are 10 seconds for versions and 30 seconds for
254
+ the listing; truncated, nonzero, signalled or timed-out output is unknown.
255
+
256
+ | Probe | Evidence |
257
+ | --- | --- |
258
+ | `node` | The first `node` on the user's real PATH (Go `exec.LookPath` order, PATHEXT on Windows), else the bootstrap one. `persistent` says which. `npm` is the runner's usable-npm proof in the user's real PATH with `$PNPM_HOME/bin` first, so a bootstrap npm never counts. A Windows `.cmd`/`.bat` cannot run with `shell:false`: a Node that resolves to one is unknown, and npm is run through what its shim runs ([Windows command shims](#windows-command-shims)). |
259
+ | `pnpm` | The runner's invocation (bootstrap handoff, or a POSIX `pnpm` on PATH). `compatible` requires a successful run (pnpm checks its Node engine at startup) at the pin or newer, of any major: the runner's commands were run with pnpm 12 against a pnpm 11 global home and back (see [pnpm 12](#pnpm-12)). `persistent` is whether any `pnpm` resolves on the real PATH. A Windows user pnpm in `$PNPM_HOME\bin` whose shim or target fails the storage walk is never run: the bootstrap's pnpm is reported in its place with `inGlobalBin: true`, `untrusted: true` and `persistent: true`, so it does not block and nothing is persisted over it. A user pnpm in `$PNPM_HOME/bin` whose shim is not recognized, or whose `--version` does not report a stable version, is handled the same way with `unchecked: true` instead, on every platform. |
260
+ | `pi`, `shell` | pnpm-global entries from the listing. A package that is not pnpm-global but whose command (`pi`, `gentle-shell`) resolves on the real PATH is unknown, never absent, so another installation is not duplicated. On Windows a `pi.cmd` or `gentle-shell.cmd` is run, and its package found, through what the shim runs; an unknown shim is never run. Shell is `usable` only when `$PNPM_HOME/bin/gentle-shell` (`.cmd` on Windows) exists. |
261
+ | `gentleAi` | Absent without gentle-pi or without its package-native binary. Otherwise compatible only for this package version, a listed path that resolves inside PNPM_HOME and `verifyGentleAi` (default `packageNativeGentleAi`) success; anything else is unknown. |
262
+ | `go` | `go version` from the real PATH; `go1.22` normalizes to `1.22.0`; devel and release-candidate builds are unknown. |
263
+ | `folders` | Windows only, after the other probes: the commands `node`, `npm`, `go`, `pi` and `gentle-shell` resolve to on the real PATH, and what each one runs, walked in one Windows PowerShell launch (`verifyWindowsStorageMany`). Returns `{ node?, npm?, go?, pi?, shell? }` with the first failing path's finding per tool, or `null`. A walk that cannot finish is `null`, never a blocker ([reused tool folders](#reused-tool-folders)). |
264
+ | `git` | Advisory only, for the standard and the bundled plan alike: `{ available }` from `git` on the real PATH, never run. On macOS `/usr/bin/git` is a shim that opens an install dialog without the Command Line Tools, so `xcode-select -p` (which never opens one) decides it; another Git first on PATH counts as it is. A missing Git adds a **Git not found** notice before consent (`gitNotice`) with the install command for the system (`sudo apt-get install -y git` on Debian or Ubuntu, `xcode-select --install`, `winget install --id Git.Git -e`). Installing does not need Git, and it is never a blocker; code review, worktrees and repository features do need it. |
265
+ | `globalBin` | `pnpmGlobalBin` over the real PATH; `writable` is write access on the nearest existing ancestor of `$PNPM_HOME/bin` (itself included). A non-directory ancestor is not writable. |
266
+ | `setup` | `false` when gentle-pi is absent. `{ available: true, recoverable: true }` for the pinned stack this pnpm installed (see [Setup recovery](#setup-recovery)), using the runner's `recoverableStackRoot`. Unknown otherwise, because an existing Shell's setup readiness has no read-only evidence. |
267
+
268
+ A failed, empty or unparseable listing makes Pi, Shell, Gentle AI and setup
269
+ unknown, which blocks planning. A package listed in two projects is ambiguous
270
+ and unknown.
271
+
272
+ ## Bundled installation
273
+
274
+ A new installation can install Gentle Shell as one self-contained product
275
+ instead: its own pinned Node.js (with the npm in the same archive), pnpm, Pi,
276
+ and Go on Windows. The user's Node.js, npm, pnpm, Go and Pi are not used or
277
+ changed. The building blocks are in [bundled install](bundled-install.md);
278
+ `scripts/bundled-wizard.mjs` wires them into the wizard.
279
+
280
+ ### When the wizard uses it
281
+
282
+ `bundledGate` decides, as a pure function, after the usual preflight. The
283
+ bundled plan is used only when all of these hold:
284
+
285
+ - The channel is `release`.
286
+ - The target is macOS, Linux or Windows on x64 or arm64.
287
+ - Preflight found no Gentle Shell (`shell.available === false`). An existing
288
+ one that a global pnpm or npm owns migrates instead (`migrationGate`, see
289
+ [Migration from a global install](bundled-install.md#migration-from-a-global-install)).
290
+ Any other existing or unknown one keeps the current path.
291
+ - This package's own release, `v<package version>`, publishes both
292
+ [distribution assets](bundled-install.md#release-distribution-assets), and
293
+ `readDistribution` accepts them.
294
+
295
+ The assets are fetched from
296
+ `https://github.com/Gentleman-Programming/gentle-shell/releases/download/v<version>/`:
297
+
298
+ - The download asks for the identity encoding.
299
+ - It is bounded to 64 KiB for the manifest and 8 MiB for the lockfile.
300
+ - It follows a redirect only to an https GitHub asset host.
301
+
302
+ | Fetch result | Plan |
303
+ |---|---|
304
+ | Both assets read | Bundled |
305
+ | 404 for either asset (v4.0.0 has none) | Standard, unchanged |
306
+ | Network failure, or assets that do not read | Standard, unchanged |
307
+
308
+ The main channel, and existing installations that neither pnpm nor npm owns,
309
+ keep the standard runner.
310
+
311
+ ### Plan and steps
312
+
313
+ The plan has the standard plan's shape, with no tools or blockers, plus
314
+ `bundled`. That field holds:
315
+
316
+ - the exact versions and the lockfile's sha256;
317
+ - the prefix and its `bin` folder;
318
+ - the pinned runtimes;
319
+ - the single PATH change, from `pathEntryPlan`.
320
+
321
+ The review screen says, before consent, that the user's own tools are not used
322
+ or changed and that their npm registry, authentication and proxy settings are
323
+ respected.
324
+
325
+ `runBundledInstall` checks that the consented plan matches the fetched assets.
326
+ It then runs and logs these steps, in this order:
327
+
328
+ | Step | What it does |
329
+ |---|---|
330
+ | `claim-prefix` | `claimPrefix`: `~/.gentle-shell`, or the Windows private folder. |
331
+ | `copy-npm-settings` | Writes the filtered copy of the user's npmrc (S12) into the prefix. |
332
+ | `install-runtime` | `ensureRuntime`: our Node with its npm, and pnpm. Go too on Windows. |
333
+ | `install-version` | `installVersion` from the release lockfile, `--frozen-lockfile`. |
334
+ | `activate-version` | `activateVersion`. |
335
+ | `write-launcher` | `ensureLauncher`. |
336
+ | `bundled-path` | `applyPathEntry`, only for a link, a profile line or the HKCU user Path. The change is planned again first; one that differs from the consented plan is not applied. |
337
+ | `shell-setup` | `gentle-shell setup` in `setupEnvironment`: the launcher on POSIX, and our Node with the same entry on Windows. |
338
+
339
+ A migration plan adds `verify-launcher` (the launcher's `--version` must print
340
+ both exact versions) before `bundled-path`, and ends with `remove-global-shell`.
341
+ The wizard's progress list (`expectedSteps`) follows the same order.
342
+
343
+ | PATH change | Outcome |
344
+ |---|---|
345
+ | None, or a link into a folder already on PATH | `ready` |
346
+ | A profile line or the user Path | `terminal-action-required` with `open-new-terminal` |
347
+ | A profile the installer may not edit | `terminal-action-required` with `add-path-line`; the outcome screen shows the exact line to copy and the file to add it to |
348
+
349
+ A failed `install-version` or `shell-setup` reports its last error line.
350
+
351
+ On Windows the user Path is edited by `windowsPathRegistry`:
352
+
353
+ - Windows PowerShell reads `HKCU\Environment\Path` without expanding its
354
+ `%VARIABLES%`, and keeps its value kind.
355
+ - The entry is added once, compared without case or a trailing backslash.
356
+ - A `WM_SETTINGCHANGE` broadcast lets new terminals see the change.
357
+
358
+ ### What this path does not change yet
359
+
360
+ - Preflight still probes the user's tools, as it does today. T5 removes those probes.
361
+ - On POSIX, gentle-pi 4.0.0's postinstall and `gentle-shell setup` run `go version`
362
+ from the user's PATH, which follows ours. Observed in a local run; Go is never
363
+ used for the POSIX release path.
364
+ - Gentle AI's Engram step in `gentle-shell setup` runs on its own terms. On macOS
365
+ it ran `brew install engram`. T4 (S9) owns that.
366
+
367
+ ## Standard installation runner
368
+
369
+ `scripts/installer-runner.mjs` exports `runStandardInstall({ plan, consent },
370
+ adapters)`, the fixed installation step the local wizard host
371
+ (`bin/gentle-shell-install.mjs`, below) calls after bootstrap and a fresh
372
+ preflight. It is a pure module: every process, filesystem,
373
+ environment, integrity and log effect goes through adapters supplied by trusted
374
+ local code, never through the browser.
375
+
376
+ ### Request contract
377
+
378
+ The request is exactly `{ plan, consent }`. `plan` must be an unmodified
379
+ `planPreflight` result: every action must equal a descriptor preflight can emit
380
+ (same id, kind, target and repository-derived version, no extra keys). Extra
381
+ request keys, commands, URLs, roots, environment or altered versions are
382
+ rejected as `invalid-request` before any process runs. `consent` must be the
383
+ boolean `true`; anything else returns `consent-required` with no commands. The
384
+ consent covers the whole displayed plan, including the PATH profile change and
385
+ the runtime persistence and npm prefix actions below, so the wizard must show
386
+ those changes before asking.
387
+
388
+ ### Adapters
389
+
390
+ | Adapter | Contract |
391
+ | --- | --- |
392
+ | `platform` | `linux`, `darwin` or `win32`. |
393
+ | `nodePath` | Absolute Node executable of the trusted host, used for npm proof and `gentle-shell setup`. |
394
+ | `env` | The user's environment. Never mutated; child environments are copies. |
395
+ | `home` | Optional; defaults to `HOME` (`USERPROFILE` on Windows). |
396
+ | `run(command, argv, { env, deadlineMs })` | Argv arrays with `shell:false` semantics and a hard deadline; returns `{ code, signal, timedOut, stdout }` with bounded output. |
397
+ | `fs` | Read-only `isFile`, `realpath`, `readText` (also used to read pnpm's npm shim). The runner has no delete or write operation. |
398
+ | `verifyGentleAi({ packageRoot, platform, env, home })` | Returns `{ ok: true }` only for package-native integrity. `packageNativeGentleAi` is the default implementation over `runtime/gentle-ai-binary.mjs`. |
399
+ | `log({ step, status, reason? })` | Receives step identifiers and statuses only, never raw command output or error text. |
400
+
401
+ ### Fixed sequence
402
+
403
+ No-process gates, all returning `blocked`:
404
+
405
+ 1. Valid request and explicit consent.
406
+ 2. Preflight blockers are absent. Two plans are supported. The clean-stack
407
+ plan must contain `install-pi`, `install-shell`, `setup-shell` and
408
+ `verify-readiness`, plus optional `setup-global-bin` and at most one exact
409
+ persistence variant: `persist-node` + `persist-package-managers` +
410
+ `configure-npm-prefix`, `persist-node` + `persist-npm` +
411
+ `configure-npm-prefix`, or `persist-package-managers` alone, or
412
+ `persist-npm` alone, or `persist-pnpm` alone. The
413
+ [setup recovery](#setup-recovery) plan is exactly `setup-shell` and
414
+ `verify-readiness`, plus optional `setup-global-bin`, and nothing else.
415
+ Prerequisite acquisition intents for Node and pnpm, `provision-native`, any
416
+ other persistence combination, other partial existing stacks and fully
417
+ reused stacks are `unsupported-plan`. `update-pi` (an
418
+ [older Pi](#existing-installations)) needs `tools.pi` to be `needs-update`
419
+ and never comes with `install-pi`; it may precede `install-shell` or an
420
+ update of Gentle Shell, or form a plan of its own with only `verify-readiness`.
421
+ With a reusable Gentle Shell, `install-pi` and `verify-readiness` alone are
422
+ supported too (`tools.pi` `unavailable` or `needs-install`).
423
+ 3. Go: `acquire-go` and `verify-go` come together, only when `tools.go` is
424
+ `needs-acquire` and the plan builds with Go (the main overlay,
425
+ `update-shell-main`, or any Windows plan other than a recovery or a Pi-only
426
+ plan); otherwise `unsupported-plan`. On Windows, Go must be `reusable` or
427
+ acquired in such a plan because gentle-pi's postinstall may build Gentle AI
428
+ from source (`go-required`). A recovery or a Pi-only update runs no
429
+ postinstall, so it needs no Go.
430
+ 4. `pnpmGlobalBin` resolves PNPM_HOME (`pnpm-home-unknown` otherwise) and
431
+ `nodePath` is absolute. A Windows plan with a private PNPM_HOME must name
432
+ that same folder (`pnpm-home-changed` otherwise).
433
+ 5. pnpm comes from the bootstrap handoff `GENTLE_INSTALL_PNPM_NODE` +
434
+ `GENTLE_INSTALL_PNPM_ENTRY` (both absolute), or on POSIX from a `pnpm`
435
+ executable on PATH. Windows requires the handoff because a `.cmd` shim cannot
436
+ run with `shell:false` (`pnpm-unavailable`); a native pnpm the bootstrap
437
+ reused arrives alone as the absolute `.exe` in `GENTLE_INSTALL_PNPM_COMMAND`.
438
+
439
+ Every child process receives the user's environment plus `PNPM_HOME` and
440
+ `$PNPM_HOME/bin` first on PATH. With a private Windows PNPM_HOME, the children
441
+ also get `XDG_CONFIG_HOME`, `XDG_CACHE_HOME` and `XDG_STATE_HOME` under it
442
+ (unless the user set them) and `TEMP`/`TMP` set to its `tmp` folder, and the
443
+ first step, `prepare-pnpm-home`, claims that folder before any command
444
+ ([Windows PNPM_HOME](#windows-pnpm_home)).
445
+
446
+ Pre-install checks, returning `blocked` on a false result or adapter error:
447
+
448
+ 1. `check-npm`: Gentle AI's normal Pi install always runs an Engram init step
449
+ that can use `npm exec` and stops on failure. The runner resolves `npm` the
450
+ way Go's `exec.LookPath` (used by Gentle AI) does: absolute child PATH
451
+ directories in order and, on Windows, every child PATHEXT extension in
452
+ PATHEXT order (case-insensitive keys; default `.COM;.EXE;.BAT;.CMD`). On
453
+ POSIX that first candidate is accepted when it works the way Gentle AI
454
+ will run it, whatever installed it (Node.js, Homebrew, nvm, fnm, or a
455
+ mise, asdf or Volta shim): run directly from `/` in the child environment
456
+ (30-second deadline each), `npm --version` must print a stable version and
457
+ `npm config get prefix` a single absolute path. Windows applies the same
458
+ behavior from the drive root (`C:\`) to an `npm.exe` (a Volta or mise
459
+ shim) run as it is, or to what a known `npm.cmd` runs: Node.js's own
460
+ `npm.cmd` (also behind nvm-windows or fnm) or an npm cmd-shim, never
461
+ through cmd.exe ([Windows command shims](#windows-command-shims)). Any
462
+ other `npm.cmd`, such as mise's file shim, is not usable, so the plan
463
+ persists the installer's npm.
464
+ A candidate in `$PNPM_HOME/bin` must instead be pnpm's global npm: a POSIX
465
+ symlink, or a shim whose single quoted `npm-cli.js` target (`$basedir/...`
466
+ on POSIX, `%~dp0\...` or `%dp0%\...` on Windows, or absolute), must
467
+ resolve (realpath) to `node_modules/npm/bin/npm-cli.js` inside PNPM_HOME,
468
+ and the package must be exactly `npm@11.19.0`. Two different targets, a
469
+ target outside PNPM_HOME or another version are rejected. When the plan
470
+ adds npm, this check runs after the add and fails as a mutating step. On
471
+ Windows an `npm.com`, `npm.bat` or other non-`.cmd`/`.exe` npm that
472
+ resolves first, in the same or an earlier directory, blocks as
473
+ `npm-shadowed`; any other mismatch blocks as `npm-unavailable`. No npm
474
+ wrapper is created.
475
+ 2. `check-global-bin`: `pnpm bin -g` must succeed and equal `$PNPM_HOME/bin`
476
+ (`global-bin-mismatch`; case-insensitive on Windows).
477
+ 3. `check-existing-stack`: `pnpm list -g --depth 0 --json` runs before any
478
+ mutation. If `@earendil-works/pi-coding-agent` or `gentle-pi` appears in the
479
+ dependencies, devDependencies or optionalDependencies of any listed project,
480
+ the runner blocks as `existing-stack` and never overwrites it, whatever the
481
+ caller's plan says. A failed, timed-out or unparseable listing, or an
482
+ unexpected JSON shape, blocks as `global-list-unavailable`. A recovery runs
483
+ `check-recoverable-stack` instead (see [Setup recovery](#setup-recovery)).
484
+
485
+ Mutating and verification steps, returning `failed` with `failedStep` and the
486
+ `completed` step list. When the plan acquires the pinned Go, `acquire-go` and
487
+ `verify-go` run first, after `check-global-bin` and `check-existing-stack`
488
+ ([Pinned Go](#pinned-go)). When it contains a persistence variant, its
489
+ [runtime persistence](#runtime-persistence) steps run next:
490
+
491
+ 1. `install-global`: exactly one
492
+ `pnpm add -g @earendil-works/pi-coding-agent@1.2.0 gentle-pi@<package version> --allow-build=gentle-pi`.
493
+ pnpm 11 installs each argument as its own global group, so gentle-pi gets
494
+ its optional peer Pi beside it, possibly newer than 1.2.0
495
+ ([The Pi that Gentle Shell runs](#the-pi-that-gentle-shell-runs)). pnpm 11
496
+ silently skips global postinstall scripts by default; the package-scoped
497
+ approval runs only gentle-pi's postinstall, which provisions Gentle AI. No
498
+ blanket build approval is ever passed.
499
+ 2. `verify-global-list`: `pnpm list -g --depth 0 --json` must report exactly the
500
+ two pinned versions in the single listed project that owns gentle-pi
501
+ (other projects, such as the persisted npm and pnpm, are ignored), and
502
+ gentle-pi's absolute `path` must resolve inside PNPM_HOME. The Pi the
503
+ launcher runs from that real path, as Node resolves it (gentle-pi's own
504
+ `node_modules` first, then beside gentle-pi), must be a
505
+ `@earendil-works/pi-coding-agent` package at a stable version at least the
506
+ minimum (1.2.0); a missing, unreadable, prerelease or older one fails. When
507
+ it is not 1.2.0, the outcome reports it as `piVersion` and the wizard says
508
+ which Pi Gentle Shell runs.
509
+ 3. `verify-shell-bin`: `$PNPM_HOME/bin/gentle-shell` (`gentle-shell.cmd` on
510
+ Windows) exists.
511
+ 4. `verify-gentle-ai`: package-native integrity of the installed package. A
512
+ declared development override (`GENTLE_PI_GENTLE_AI_DEV_BINARY` or its
513
+ registration file) is never package-native proof and fails.
514
+ 5. `shell-setup`: the installed public CLI, `node <gentle-pi>/bin/gentle-shell.mjs
515
+ setup`, with the child environment minus every `GENTLE_BOOTSTRAP_*` and
516
+ `GENTLE_INSTALL_*` key (case-insensitive on Windows); pnpm commands keep the
517
+ handoff. Nonzero, signal or deadline fails. The runner neither duplicates
518
+ setup logic nor writes the automatic provisioning marker. It requests the
519
+ last 4 KiB of setup's stderr, where `gentle-shell setup` reports errors, and
520
+ a failure carries `detail`: the last error line (containing `Error:` or a
521
+ pnpm `ERR_` code), else the last non-empty line, without terminal escapes,
522
+ control or bidi characters, with the user's home replaced by `~` and at most
523
+ 300 characters (`setupErrorDetail`). The raw output is not kept; only this
524
+ step, `persist-path`, `acquire-go` (the Go folder in its way), `install-global`,
525
+ the main steps `build-gentle-ai-main` and `install-shell-main`, and the update's
526
+ `update-shell` have a detail. A failed `pnpm add -g` (`install-global`, or the
527
+ main package's) takes it from the same 4-KiB stderr tail or, when that has no
528
+ line, from stdout, where pnpm reports a failed postinstall; the postinstall
529
+ prints its error's whole `cause` chain and ends with the root cause's last
530
+ output line as `root cause Error: …`, so that line is the one selected. A main
531
+ step or the update takes it only from a `MainChannelError`: its message, then
532
+ the failed `go install` or extraction's stderr tail kept as its cause (for
533
+ example `main-commit-unavailable: … (HTTP 403)` when GitHub refuses the latest
534
+ commit). Any other error's text is never kept.
535
+ 6. `persist-path`, only when `$PNPM_HOME/bin` was not on the user's own PATH:
536
+ `pnpm setup`, then the outcome is `terminal-action-required` with
537
+ `action: "open-new-terminal"`. A child environment never proves that a fresh
538
+ terminal resolves `gentle-shell`. On POSIX, `pnpm setup` picks the profile
539
+ to edit from the inherited `SHELL` variable (bash, zsh, fish, ksh, dash, sh
540
+ or nushell); without it pnpm 11.1.1 fails with `ERR_PNPM_UNKNOWN_SHELL`,
541
+ with another shell `ERR_PNPM_UNSUPPORTED_SHELL`. A terminal session always
542
+ exports `SHELL`; a launcher that does not (for example `docker exec`, some
543
+ service managers or IDE tasks) makes this step fail. pnpm prints these
544
+ errors on stdout, so a failure takes `detail` from the same 4-KiB stderr
545
+ tail or, when that has no line, from stdout. Before checking the shell, the
546
+ observed pnpm 11.1.1 also installs the latest `@pnpm/exe` globally ("Installing
547
+ pnpm CLI globally"), which is upstream behavior the runner does not control.
548
+
549
+ `ready` requires every step above to complete and the global bin to already be
550
+ on the user's PATH. Skipped, unverified or failed mandatory steps never yield
551
+ `ready`, and no existing installation, home or data is deleted on any outcome.
552
+ Deadlines are 30 seconds for probes and npm config commands, and 20 minutes
553
+ each for the install, persistence and setup steps, including `pnpm setup`,
554
+ which installs `@pnpm/exe` from the registry.
555
+
556
+ ### Setup recovery
557
+
558
+ `gentle-shell setup` runs after `add -g`, so a setup that stops (for example on
559
+ the [GitHub API limit](#github-api-limit-during-setup)) or a failed `pnpm setup`
560
+ leaves Pi and gentle-pi installed. Rerunning the installer then completes that
561
+ installation instead of blocking as an existing stack.
562
+
563
+ Detection is the setup probe, with the same rule the runner applies
564
+ (`recoverableStackRoot`): the global listing names Pi and gentle-pi exactly
565
+ once each, both in the single project that owns gentle-pi, Pi at exactly
566
+ 1.2.0 and gentle-pi at exactly this package's version, gentle-pi's path
567
+ resolves (realpath) inside PNPM_HOME, and the Pi beside gentle-pi is at least
568
+ the minimum. Preflight also requires Pi, Shell and
569
+ Gentle AI to be `reusable`. Anything else (another version, a second listing,
570
+ a path outside PNPM_HOME, Pi missing) keeps the setup probe unknown, so a
571
+ foreign or other-version stack stays blocked exactly as before.
572
+
573
+ The recovery plan is all or nothing: `setup-shell`, `verify-readiness` and
574
+ `setup-global-bin` only when the global bin is off PATH. The runner then runs
575
+ `check-npm`, `check-global-bin` and `check-recoverable-stack`, which lists the
576
+ global packages again and blocks as `existing-stack-unverified` when the stack
577
+ no longer matches (or as `global-list-unavailable` when pnpm cannot list).
578
+ It skips `install-global` (never `add -g`) and continues with the unchanged
579
+ `verify-global-list`, `verify-shell-bin`, `verify-gentle-ai`, `shell-setup`
580
+ and, when planned, `persist-path`, with the same outcomes. A recovery never
581
+ persists the runtime: the earlier run persisted it before installing the
582
+ stack, and `check-npm` still proves a usable npm in the child environment.
583
+
584
+ Rerunning `gentle-shell setup` is acceptable because it is the public,
585
+ rerunnable command the clean installation already runs; the runner adds no
586
+ setup logic of its own. The review screen says "Gentle Shell is already
587
+ installed; this completes setup" and offers **Complete setup**.
588
+
589
+ ### Runtime persistence
590
+
591
+ A bootstrap-acquired Node lives in a temporary tools directory, so a later
592
+ terminal would not find `node` or the working npm that Gentle AI's Engram step
593
+ needs. Every persistence step uses the same pnpm argv prefix and child
594
+ environment as the stack install, and the runner persists only what is missing.
595
+
596
+ For a bootstrap-only Node, the full group runs these fixed steps:
597
+
598
+ 1. `persist-node`: `pnpm runtime set node 24.21.0 -g`, which installs Node under
599
+ PNPM_HOME and links `node` into `$PNPM_HOME/bin`.
600
+ 2. `persist-package-managers`: `pnpm add -g npm@11.19.0 pnpm@11.1.1`, which
601
+ adds the `npm`, `npx` and `pnpm` shims to `$PNPM_HOME/bin`. No build
602
+ approval of any kind is passed. When pnpm is already persistent (a newer
603
+ pnpm 11 in `$PNPM_HOME/bin`, for example), the plan has `persist-npm`
604
+ instead: `pnpm add -g npm@11.19.0` only, so that pnpm is never downgraded.
605
+ 3. `verify-persistent-runtime`: in the child environment both `node` and `npm`
606
+ must resolve (`exec.LookPath` order) from `$PNPM_HOME/bin`, the node must be
607
+ spawnable (`.exe`/`.com` on Windows) and print `v24.21.0`.
608
+ 4. `check-npm`: the npm gate above. `$PNPM_HOME/bin` is first in the child
609
+ environment, so it resolves the pinned shim, never a user npm.
610
+ 5. `configure-npm-prefix`: with a pnpm-managed node, npm's default prefix is
611
+ derived from a path inside pnpm's store, so `npm install -g` would write
612
+ there. The runner reads `npm config get prefix` with the persisted node.
613
+ Unless an `npm_config_prefix` (any case) or `PREFIX` variable is set, it
614
+ asks pnpm for its store with `pnpm store path` (30-second deadline); a
615
+ failed, non-absolute or unresolvable answer fails this step rather than
616
+ assuming `$PNPM_HOME/store`. Only when the effective prefix resolves inside
617
+ that store does it run
618
+ `npm config set prefix <PNPM_HOME> --location=user`, then require the
619
+ effective prefix to equal PNPM_HOME. An explicit user prefix elsewhere
620
+ makes npm report that prefix, so it is never overwritten. A failed read,
621
+ write or verification fails this step with the completed list.
622
+
623
+ The outcome then also reports `npmPrefix: "configured"` or `"unchanged"`.
624
+
625
+ A persistent Node is never replaced or shadowed: there is no `runtime set` and
626
+ no prefix change. An older Node on the user's PATH is not a persistent Node
627
+ here: the bootstrap's pinned Node runs the wizard, so the full group above
628
+ persists it next to the older one, which is left unchanged. One fixed add runs, then only the matching checks:
629
+
630
+ | Intent | Command | Then |
631
+ | --- | --- | --- |
632
+ | `persist-npm` | `pnpm add -g npm@11.19.0` | `check-npm` |
633
+ | `persist-pnpm` | `pnpm add -g pnpm@11.1.1` | `verify-persistent-pnpm` |
634
+ | `persist-package-managers` | `pnpm add -g npm@11.19.0 pnpm@11.1.1` | `check-npm`, `verify-persistent-pnpm` |
635
+
636
+ `check-npm` is the npm gate above. `verify-persistent-pnpm` requires
637
+ the first `pnpm` in the child environment to be in `$PNPM_HOME/bin` (`.cmd`
638
+ on Windows), resolving like the npm shim to `node_modules/pnpm/bin/pnpm.mjs`
639
+ inside PNPM_HOME, with `package.json` `pnpm@11.1.1`, and `node pnpm.mjs
640
+ --version` printing `11.1.1`. When pnpm is not added, `check-npm` stays a
641
+ pre-install gate.
642
+ `npm config get` ignores `--location`, and `.npmrc` files can hold credentials,
643
+ so the runner never reads them: a user-level prefix that itself points into
644
+ the store is indistinguishable from the default and would be replaced.
645
+
646
+ ### Existing installations
647
+
648
+ The wizard installs what is missing and updates what exists; it updates an older
649
+ Pi but never reinstalls or downgrades it, and nothing changes before consent.
650
+
651
+ | Found | Plan |
652
+ | --- | --- |
653
+ | A compatible Pi (pnpm-global, or any `pi` on PATH whose `pi --version` reports a stable version ≥ the minimum) and no Gentle Shell | Install only Gentle Shell: `pnpm add -g gentle-pi@<version> --allow-build=gentle-pi`, after checking that pnpm lists no gentle-pi (`check-existing-shell`). Pi is left as it is, also when it is newer than 1.2.0. |
654
+ | A Gentle Shell that pnpm or npm owns | `update-shell-release` when it is older, unusable or a main build; `update-shell-main` on the main channel (needs Go). Then `setup-shell`. A missing Pi is installed first (`install-pi`). |
655
+ | A current Gentle Shell that npm owns, on release | Nothing to do. |
656
+ | A Pi older than the minimum that pnpm or npm owns | `update-pi` to `PI_INSTALL_VERSION` with that package manager (`pnpm add -g @earendil-works/pi-coding-agent@<version>` or `npm install -g …`), before any Gentle Shell step: ahead of `install-shell` or `update-shell-*`, or alone when Gentle Shell is current. The plan names the found and target versions and the manager. |
657
+ | A Pi older than the minimum that neither pnpm nor npm owns (mise, Homebrew, a standalone binary) | Left unchanged. `install-pi` adds the installer's Pi with pnpm exactly as when Pi is absent (in the same `pnpm add -g` as gentle-pi, before an update of Gentle Shell, or alone when Gentle Shell is current), and the plan says so before consent. |
658
+ | A Gentle Shell neither pnpm nor npm owns (an `npm link` of a source checkout, for example) | Blocked with an explanation; never reinstalled. |
659
+ | A Node.js older than the minimum with a stable version (mise, nvm, Homebrew, an old installer) | Left unchanged. The POSIX bootstrap acquires its verified Node 24.21.0 as when Node is absent, and the plan persists it (`persist-node`, `persist-package-managers`, `configure-npm-prefix`). The plan says so before consent. An unknown or prerelease version still blocks. Windows: `bootstrap.cmd` does the same, and also acquires its Node for a user Node whose storage it cannot trust (nvm-windows' symlink, fnm's junction), which it never runs. |
660
+ | A pnpm older than 11.1.1, or whose engine rejects the bootstrap's Node (or, for the bootstraps only, another major) | Left unchanged. Both bootstraps acquire the verified pnpm 11.1.1 as when pnpm is absent, and the plan persists it (`persist-pnpm`, or `persist-package-managers` with npm). The plan says so before consent. Unknown evidence still blocks. |
661
+ | That pnpm in `$PNPM_HOME/bin` itself (where persisting pnpm writes) | Never replaced or downgraded. At 11.1.1 or newer, of any major (pnpm 12 included), it is used as it is and nothing is persisted over it. An older one blocks before consent; the guidance names the found and required versions and updates it with `pnpm self-update 11.1.1`. |
662
+ | A pnpm in `$PNPM_HOME/bin` whose shim is not recognized or whose `pnpm --version` reports no stable version | Left unchanged and never run again. The bootstrap's verified pnpm runs the installer and nothing is persisted over it (no `persist-pnpm`). The plan says so before consent. |
663
+ | An older Node.js or incompatible pnpm next to an existing Gentle Shell (updated or current) | Blocked before consent: runtimes are persisted only while installing Gentle Shell. A setup recovery is not blocked and persists nothing. |
664
+ | A Go missing or older than the minimum (main channel, or a Windows build) | Left unchanged. The installer downloads its [pinned Go](#pinned-go) only for the build, and the plan says so before consent with the found version (or missing). A Go whose version cannot be checked gets the pinned Go the same way, and is never run for the build. A release install on macOS or Linux never downloads Go. |
665
+
666
+ Ownership comes from real paths ([`installOwner`](../scripts/main-channel.mjs)):
667
+ pnpm when the package lives under PNPM_HOME, npm only when it is
668
+ `<npm root -g>/gentle-pi` itself. On Windows the probe follows
669
+ `gentle-shell.cmd` to the package it runs, and npm and pnpm never run as a
670
+ `.cmd`: `runUpgrade` takes an `invocation(name)` adapter
671
+ (`upgradeInvocation`) that runs npm through what `npm.cmd` runs
672
+ (`npmInvocation`) and pnpm from the bootstrap handoff in the wizard, or through
673
+ `windowsInvocation` of the first `pnpm` on PATH in `gentle-shell upgrade`. A
674
+ manager that resolves only to a `.cmd` or `.bat` counts as missing. On macOS
675
+ and Linux nothing changes: the command on PATH runs as it is. The update runs
676
+ the same code as `gentle-shell upgrade --channel <channel>`
677
+ (`check-installed-shell`, `update-shell`), then `verify-updated-shell` requires
678
+ the same owner and a stable version not older than before (release) or a
679
+ `-main.<sha12>` version (main), and for pnpm a Pi beside the updated
680
+ gentle-pi at least the minimum (its version is reported when it is not 1.2.0);
681
+ release also re-runs `verify-gentle-ai`. An
682
+ update never runs `pnpm setup`: the existing installation already has its PATH.
683
+
684
+ An older Pi uses the same ownership rule with its own package name
685
+ (`<npm root -g>/@earendil-works/pi-coding-agent` for npm, on every platform:
686
+ Windows runs `npm root -g` and `npm install -g` through what `npm.cmd` runs and
687
+ compares roots case-insensitively). Its probe reports
688
+ `owner` only when Pi is older than the minimum, and the plan records the found
689
+ version and owner in `tools.pi`. Before any change, `check-installed-pi` finds
690
+ Pi again and requires that same version and owner, a stable version below the
691
+ minimum (so the update never downgrades), and for npm an `npm root -g` that
692
+ holds it. `update-pi` then runs `pnpm add -g` through the runner's pnpm, or the
693
+ user's `npm install -g` with the user's environment. `verify-updated-pi`
694
+ requires one Pi from the same owner (pnpm listing it twice makes it ambiguous),
695
+ at `PI_INSTALL_VERSION` or newer, and for npm at the same root. A failure stops
696
+ before any Gentle Shell step. With pnpm 11.1.1, `pnpm add -g` of a newer Pi
697
+ replaces the existing global package in place (its isolated global directory is
698
+ swapped), whether Pi was added alone or together with gentle-pi, so the next run
699
+ sees a single Pi; `npm install -g` replaces it in npm's global root.
700
+
701
+ An older Node.js or incompatible pnpm is never changed or removed, and once the
702
+ bootstrap has its pinned copy the installer never runs it for installation
703
+ steps: the wizard and runner run under the bootstrap's Node (`process.execPath`),
704
+ `persist-node` puts the pinned Node first in the runner's child `PATH`, and the
705
+ runner invokes the bootstrap's pnpm by absolute path (`runnerEnvironment` puts
706
+ its directory first on POSIX; Windows uses the `GENTLE_INSTALL_PNPM_*`
707
+ handoff). The wizard reads the user's pnpm only for its `--version` (POSIX,
708
+ from `/`). The plan's runtime persistence text names the found and pinned
709
+ versions and says the user's copy is left unchanged. It promises no PATH
710
+ precedence: a version manager such as `mise activate` puts its own paths first
711
+ again on every prompt, so new terminals may still run the older Node or pnpm.
712
+ An update of an existing Gentle Shell persists nothing, so an older Node or
713
+ incompatible pnpm still blocks it (`incompatible-tool`).
714
+
715
+ An older Pi that neither pnpm nor npm owns is never run, changed or removed. The
716
+ plan's `install-pi` description names its version and the version the installer
717
+ adds. Alone, `install-pi` is checked like a shell-only installation:
718
+ `check-existing-pi` requires pnpm to list no Pi, `pnpm add -g` adds it, and
719
+ `verify-installed-pi` requires one pnpm-global Pi at `PI_INSTALL_VERSION` or
720
+ newer. The `pi` probe reads pnpm's global list before PATH, so the next run
721
+ reuses the pnpm-global Pi and no longer looks at the other one. Gentle Shell
722
+ prefers the Pi beside gentle-pi over any `pi` on PATH
723
+ ([The Pi that Gentle Shell runs](#the-pi-that-gentle-shell-runs)); a
724
+ terminal's `pi` command may still run the older one when it comes first on PATH.
725
+
726
+ ### The Pi that Gentle Shell runs
727
+
728
+ The launcher runs the Pi that Node resolves from gentle-pi (gentle-pi's own
729
+ `node_modules`, then beside it), and only without one the first `pi` on PATH.
730
+ pnpm 11 installs every `pnpm add -g` argument in its own isolated global
731
+ directory (group) and auto-installs gentle-pi's optional peer Pi in gentle-pi's
732
+ group at the latest version: verified with pnpm 11.1.1 in an isolated
733
+ PNPM_HOME, `pnpm add -g @earendil-works/pi-coding-agent@1.0.0 gentle-pi@4.0.0`
734
+ lists Pi 1.0.0 while the Pi beside gentle-pi is 1.1.0. So Gentle Shell may run a
735
+ Pi newer than `PI_INSTALL_VERSION`, always at least the minimum: the runner
736
+ verifies that Pi (`verify-global-list`, the setup recovery, a pnpm
737
+ `verify-updated-shell`) and, when it is not `PI_INSTALL_VERSION` (1.2.0 now; the 1.0.0/1.1.0 figures above are that older verification), the outcome says which Pi
738
+ Gentle Shell runs. `--config.auto-install-peers=false` would leave gentle-pi
739
+ with no Pi beside it, so the launcher would run any `pi` on PATH, and
740
+ `--config.overrides` is ignored for global installs.
741
+
742
+ Pi and gentle-pi stay separate pnpm groups on every path (the clean install,
743
+ the main package and `gentle-shell upgrade`). A single comma-separated group
744
+ (`pnpm add -g <pi>,gentle-pi@<version>`) would pin the Pi beside gentle-pi, but
745
+ pnpm replaces every existing group that shares a package with a new one: Pi's
746
+ own `pi update` (`pnpm install -g @earendil-works/pi-coding-agent@<latest>`)
747
+ then deleted the whole group, gentle-pi and its `gentle-shell` command
748
+ included, and the group also replaced unrelated packages sharing the user's Pi
749
+ group. User decision (2026-10-10): "Quitar el agrupamiento (recomendado)".
750
+ Pinning the Pi gentle-pi runs at the package level (an exact gentle-pi
751
+ dependency instead of the host peer) needs its own design.
752
+
753
+ ### Documented decisions
754
+
755
+ - `check-npm` failing after `persist-npm` (a reported case) no longer occurs in
756
+ that setup: on POSIX a working user npm (mise, Volta, asdf and others) is
757
+ accepted, so `persist-npm` is not planned for it. The failure itself cannot
758
+ be root-caused without the reporter's `PNPM_HOME`, `pnpm store path` and
759
+ `readlink -f $PNPM_HOME/bin/npm`, so nothing else changes until that evidence
760
+ exists.
761
+ - <a id="pnpm-12"></a>A pnpm 12 in `$PNPM_HOME/bin` no longer blocks. Measured on
762
+ macOS with pnpm 12.11.2 (`pnpm self-update` into an isolated `PNPM_HOME`): the
763
+ bootstrap's pnpm 11.1.1 ran `bin -g`, `store path`, `add -g` of Pi and of
764
+ `gentle-pi --allow-build=gentle-pi`, and `list -g --depth 0 --json` there; then
765
+ pnpm 12 listed, re-added (the `gentle-shell upgrade` path) and removed the same
766
+ packages, and pnpm 11 still read the global home. Both majors share
767
+ `store/v11` and `global/v11`. With pnpm 12 alone, `runtime set node … -g`,
768
+ `add -g npm@…`, Pi and `gentle-pi` also succeeded. pnpm 12 refuses
769
+ `add -g pnpm` (`ERR_PNPM_GLOBAL_PNPM_INSTALL`); the installer never persists
770
+ pnpm over a compatible one, so that command is not run.
771
+
772
+ ### Main channel
773
+
774
+ The Review screen offers **Latest release** (default) or **Latest main**. Choosing
775
+ main reloads the plan with `GET /api/plan?channel=main`; the install request is
776
+ unchanged because the consented plan already holds its steps. Neither repository
777
+ publishes main builds, so main is built on this computer
778
+ ([`scripts/main-channel.mjs`](../scripts/main-channel.mjs)):
779
+
780
+ - Preflight reuses a Go ≥ the Windows minimum on every platform; a missing or
781
+ older Go, or one whose version cannot be checked, gets the
782
+ [pinned Go](#pinned-go), first on the build's PATH. Main steps are added only when the
783
+ plan installs or completes setup; a stack that is already set up switches with
784
+ `gentle-shell upgrade --channel main`, or the wizard's update plan below.
785
+ - After the release stack is installed and `verify-gentle-ai` passed, the runner:
786
+ 1. `build-gentle-ai-main`: resolves the latest `main` commit of
787
+ Gentleman-Programming/gentle-ai (GitHub API, raw SHA), runs
788
+ `go install github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai@<sha>`
789
+ in a sealed Go environment (`GOPROXY=https://proxy.golang.org`,
790
+ `GOSUMDB=sum.golang.org`, `GOTOOLCHAIN=local`, `GOFLAGS=-modcacherw`), requires
791
+ the binary's version to end in that commit's first 12 characters, keeps it
792
+ under `<config home>/main/gentle-ai/<sha>/` and registers it as the
793
+ `gentle-pi.dev-binary/v1` override, which never falls back to the pinned
794
+ binary silently.
795
+ 2. `install-shell-main`: downloads the latest `main` commit's source tarball,
796
+ extracts it (on Windows with `%SystemRoot%\System32\tar.exe` by absolute
797
+ path, never a `tar` from PATH: Git for Windows' MSYS tar in `usr\bin` reads
798
+ a `D:\…` archive path as a remote host and fails with `Cannot connect to D:
799
+ resolve failed`), sets its version to `<version>-main.<sha12>`, removes `prepack` and `prepare`
800
+ (which would run the full test suite), packs it with `pnpm pack` into
801
+ `<config home>/main/packages/`, runs `pnpm add -g <tgz> --allow-build=gentle-pi`
802
+ and requires `pnpm list -g` to report that exact version under PNPM_HOME,
803
+ with a Pi at least the minimum beside it.
804
+ Installing `github:` or codeload URLs directly is not used: pnpm runs
805
+ `prepack` for both.
806
+ 3. `record-channel`: writes `{"schema":"gentle-shell.channel/v1","channel":"main",
807
+ "shellCommit":…,"gentleAiCommit":…}` to `<config home>/channel.json`.
808
+ - `shell-setup` then runs from the main package. The config home is
809
+ `GENTLE_PI_CONFIG_HOME` or `~/.pi/gentle-ai`, as for the dev-binary override.
810
+ - `gentle-shell upgrade --channel main` builds with the user's Go only when it
811
+ reports a stable version that meets the minimum. Otherwise (missing, older, or
812
+ a version it cannot check) it builds with the pinned Go under
813
+ `<config home>/tools/go`, downloading and verifying it there with the same
814
+ `acquireGo` as the wizard when no earlier run published it, and only when
815
+ Gentle AI is rebuilt. A failed download stops with `main-requires-tools`
816
+ before anything is built; the user's Go is never run for the build.
817
+ - Trust: Gentle AI source is verified by Go's checksum database; the Gentle Shell
818
+ source is fetched over TLS from GitHub by exact commit SHA, with no pinned
819
+ digest. Main builds are development builds.
820
+
821
+ ### Pinned Go
822
+
823
+ Go is needed only after the wizard knows the channel, so it is not a bootstrap
824
+ prerequisite: Node and pnpm are acquired by the bootstraps because the wizard
825
+ cannot run without them, while Go is acquired by the runner after consent, as the
826
+ first mutating steps of a plan that builds with it, before anything is persisted
827
+ or installed. Nothing is downloaded before consent, and a failed download leaves
828
+ the computer as it was.
829
+
830
+ - `acquire-go`: [`acquireGo`](../scripts/installer-downloads.mjs) downloads the
831
+ official go.dev archive for the platform (darwin, linux and windows; amd64 and
832
+ arm64), pinned by exact version (Go 1.25.14), size and SHA-256 from go.dev's
833
+ published checksums (`https://dl.google.com/go/<file>`, where go.dev/dl
834
+ redirects). The bytes are verified before they are read. The `.tar.gz` (POSIX
835
+ ustar, PAX `path` only) or Windows `.zip` (stored or deflated, CRC-checked) is
836
+ extracted in process into a private staging directory: only regular files and
837
+ directories under `go/`, with no links, traversal, absolute paths or duplicate
838
+ names, and `go/VERSION` must name the pin. The tree is published without
839
+ replacing anything as `<config home>/tools/go/<version>/go` (directory `0700`):
840
+ the marker naming the archive is written into the staging directory, which then
841
+ becomes `<version>` in one rename, so that folder never exists unmarked. A
842
+ later run reuses a marked copy without downloading. Any mismatch fails the step
843
+ and publishes nothing. An existing `<version>` folder without the marker (left
844
+ by an interrupted run of an earlier installer, or not the installer's) is never
845
+ replaced: the step fails before downloading, and the wizard shows that folder
846
+ (home as `~`) with guidance to remove it.
847
+ - `verify-go`: the published `go` (by absolute path, `GOTOOLCHAIN=local`) must
848
+ print `go version go<pin> …`.
849
+ - The pinned Go is never put on the user's PATH or profile, and the user's Go
850
+ (Homebrew, mise, an official package, anything) is only asked its version
851
+ (`go version`, as the preflight probe always did), never changed or removed. The runner passes it by path to `build-gentle-ai-main` and puts its
852
+ `bin` directory first on PATH only for the children that may build Gentle AI:
853
+ `pnpm add -g` (gentle-pi's Windows postinstall finds `go.exe` there) and the
854
+ wizard's Gentle Shell update.
855
+ - The plan copy before consent names the pinned version, the Go found (or
856
+ missing), and that the user's Go, PATH and shell profile stay unchanged.
857
+
858
+ ### Remaining T7 real-machine checks
859
+
860
+ The runner is verified only with deterministic fake adapters. Observed pnpm
861
+ 11.1.1 facts come from one Linux laboratory run. Before claiming support, T7
862
+ must establish on real Windows, macOS and Linux machines:
863
+
864
+ - `pnpm list -g --json` reports a dependency `path` that resolves inside
865
+ PNPM_HOME for the installed layout, and prints a parseable JSON array (for
866
+ example `[]`) when no global package exists yet; empty output would block
867
+ a clean installation as `global-list-unavailable`;
868
+ - Windows npm resolution matches Gentle AI's actual lookup, including its
869
+ current-directory handling;
870
+ - the scoped `--allow-build=gentle-pi` postinstall provisions package-native
871
+ Gentle AI, including Windows source builds with the user's Go;
872
+ - `gentle-shell setup` completes the Engram init step with the accepted npm, and
873
+ whether Gentle AI selects `pnpm dlx` or `npm exec`;
874
+ - `pnpm setup` makes `gentle-shell` resolvable in a fresh terminal of each
875
+ supported shell (the Linux lab showed it does not make `pnpm` itself
876
+ resolvable);
877
+ - the chosen deadlines suffice on slow networks, and pnpm's registry and
878
+ update-notice traffic is acceptable;
879
+ - interrupted installs are recoverable by rerunning the wizard without data loss;
880
+ - `pnpm runtime set node -g` on Windows links a spawnable `node.exe` (a `.cmd`
881
+ shim fails `verify-persistent-runtime`), and pnpm's real `npm.cmd`/POSIX
882
+ shim text matches the accepted target forms;
883
+ - npm's Windows global layout: it places executables in the prefix itself, so
884
+ a PNPM_HOME prefix may not put `npm install -g` executables on PATH; also
885
+ whether Windows npm's default prefix lands in the store at all;
886
+ - `pnpm store path` reports a directory (for example `$PNPM_HOME/store/v11`)
887
+ that actually contains the effective npm prefix of a pnpm-managed node;
888
+ - pnpm's real global `pnpm` shim targets `node_modules/pnpm/bin/pnpm.mjs`, and
889
+ a persistent Node missing npm has a prefix outside the store (the variant
890
+ without `runtime set` configures no prefix);
891
+ - download integrity of the Node runtime and the npm and pnpm tarballs fetched
892
+ during persistence (the Linux lab saw a lockfile hash matching the Node pin,
893
+ not download-time verification);
894
+ - pnpm's update notifier contacting registry.npmjs.org during these commands;
895
+ - `pnpm list -g --json` listing persisted npm/pnpm and the stack as separate
896
+ projects with a usable gentle-pi `path`;
897
+ - the probes' `access(W_OK)` writability check, which ignores Windows ACLs, and
898
+ process deadlines that kill only the direct child.
899
+
900
+ ## Local wizard host and entry
901
+
902
+ `bin/gentle-shell-install.mjs` is the entry the bootstrap's `launchWizard`
903
+ starts with no argv and inherited stdio. It is not a `package.json` `bin`
904
+ command; it ships inside the package's `bin/` directory and is listed in
905
+ `scripts/verify-package-files.mjs` with the other installer files
906
+ (`installerPaths`).
907
+
908
+ ### Entry wiring
909
+
910
+ - Probes: `createProbes` over the wizard's own environment with
911
+ `hostAdapters()` (argv spawn with `shell:false`, deadlines, bounded output,
912
+ read-only fs). Each plan request creates fresh probes, then
913
+ `collectInventory` and `planPreflight` run server-side.
914
+ - Runner: `runStandardInstall` with `nodePath: process.execPath`, the same
915
+ adapters, `packageNativeGentleAi` and `runnerEnvironment`: the user's real
916
+ PATH from `userEnvironment`, so a temporary bootstrap npm or node never counts
917
+ as persistent and `onPath` stays truthful. A POSIX bootstrap pnpm, which the
918
+ bootstrap passes only through PATH, stays reachable as the **last** PATH
919
+ entry (its directory holds only the `pnpm` wrapper); Windows uses the
920
+ `GENTLE_INSTALL_PNPM_*` handoff, which `userEnvironment` keeps.
921
+ - The entry prints the session URL, then opens it through a private redirect
922
+ file so the one-time code never appears in the opener's argv (visible to
923
+ local process listings). `writeRedirect` creates a fresh `mkdtemp` directory
924
+ `gentle-shell-install-*` under the OS temp dir (mode 0700) holding
925
+ `open.html` (mode 0600, exclusive create): a static page with a meta refresh
926
+ and a link to the session URL, no script. `openBrowser` passes only that file
927
+ path to a fixed opener spawned with `shell:false` and detached:
928
+ `/usr/bin/open` on macOS, `xdg-open` on Linux, `%SystemRoot%\System32\rundll32.exe
929
+ url.dll,FileProtocolHandler` on Windows (absolute, so the current directory is
930
+ never searched). Without an opener nothing is written; a failed write opens
931
+ nothing; an opener that fails to start removes the file. The file and its
932
+ directory are removed (file, then directory, best effort) once the code is
933
+ redeemed (`onRedeemed`) or when the host closes. File modes are POSIX only;
934
+ on Windows the per-user `%TEMP%` ACLs apply. The printed URL stays the
935
+ fallback.
936
+
937
+ | Exit code | When |
938
+ | --- | --- |
939
+ | 0 | The last installation outcome was `ready` or `terminal-action-required`. Node, npm and pnpm now persist under `$PNPM_HOME` or were already the user's, so the bootstrap removes the tools directory it created. |
940
+ | 1 | `blocked`, `failed`, a shutdown or idle close without a completed installation, a signal, or a host start failure. The bootstrap removes its tools. |
941
+
942
+ SIGINT and SIGTERM close the server and exit with the code above. An
943
+ installation that is still running is not cancelled cleanly: Ctrl+C reaches the
944
+ runner's children through the terminal's process group, but SIGTERM does not.
945
+
946
+ Known debt, not fixed yet: on SIGINT or SIGTERM, `close` does not wait for a
947
+ running installation to settle, and runner deadlines kill only the direct child
948
+ process, not the processes it started (for example, the ones `pnpm` or
949
+ `gentle-shell setup` spawn).
950
+
951
+ ### Security model
952
+
953
+ `scripts/installer-server.mjs` exports `createInstallerServer({ collectPlan,
954
+ runInstall, assetsDir, now?, random?, limits?, onRedeemed? })`, a
955
+ dependency-free `node:http` host. Every effect is injected; tests use fake
956
+ plans and runners. `onRedeemed()` runs once when the one-time code is used; an
957
+ exception from it is ignored.
958
+
959
+ - Binds `127.0.0.1` on port 0 only and asserts the bound IPv4 address; never
960
+ `localhost`, `::` or `0.0.0.0`.
961
+ - Every request needs `Host` exactly `127.0.0.1:<port>`, otherwise 421 before
962
+ any authorization (a missing Host included).
963
+ - Every response carries `Content-Security-Policy: default-src 'self';
964
+ frame-ancestors 'none'; base-uri 'none'; form-action 'none'`,
965
+ `X-Content-Type-Options: nosniff`, `Cache-Control: no-store`,
966
+ `Referrer-Policy: no-referrer`, `X-Frame-Options: DENY`,
967
+ `Cross-Origin-Resource-Policy: same-origin` and
968
+ `Cross-Origin-Opener-Policy: same-origin`. No CORS header is ever sent; error
969
+ bodies are generic codes such as `{ "error": "forbidden" }`.
970
+ - Session: a 32-byte random secret becomes the cookie
971
+ `gentle_install_session` (`HttpOnly; SameSite=Strict; Path=/`). The printed
972
+ URL `http://127.0.0.1:<port>/session?code=<code>` carries a separate random
973
+ one-time code that expires after 2 minutes; a valid `GET /session` consumes it
974
+ and answers 200 with a minimal HTML page that sets the cookie and refreshes
975
+ to `/` (`<meta http-equiv="refresh" content="0; url=/">`, no script or
976
+ style, same strict headers). It is not a 303: reached from the `file:`
977
+ redirect page, a redirect chain counts as cross-site and the
978
+ `SameSite=Strict` cookie could be withheld on `/`, while a navigation started
979
+ by this same-origin page carries it. Code and cookie comparisons use
980
+ `timingSafeEqual`. Every asset and `/api/*` request needs the cookie (401).
981
+ - A request with an `Origin` other than `http://127.0.0.1:<port>` is rejected
982
+ (403), including GET. Every `/api/*` request, GET included, needs the header
983
+ `X-Gentle-Install: 1` (403 otherwise, before any probe or plan rotation), so
984
+ a cross-origin fetch always needs a CORS preflight, which never succeeds.
985
+ POST also requires that exact Origin and `Content-Type: application/json`
986
+ (415 otherwise).
987
+ - Bodies are limited to 1 KiB (413, by declared length and while streaming).
988
+ Server `headersTimeout` is 10 s and `requestTimeout` 30 s.
989
+ - The host closes after 30 minutes without activity while no installation
990
+ runs (`limits.idleMs`, checked every 15 s with the injected clock). Only a
991
+ request with the right Host and a valid session cookie, a successful code
992
+ redemption or a finished installation counts as activity; wrong-Host,
993
+ unauthenticated or bad-code traffic never extends the timeout.
994
+
995
+ ### Endpoints
996
+
997
+ Fixed allowlist, matched on the raw request path: anything else is 404, a
998
+ known path with another method is 405. Only `/session`, `/api/progress` and
999
+ `/api/plan` (exactly `channel=release` or `channel=main`) take a query string. Assets map to fixed file names in `assetsDir`; no request
1000
+ path reaches the filesystem, so traversal forms (`/../`, `%2e%2e`, `%2f`) are
1001
+ simply unknown paths.
1002
+
1003
+ | Route | Contract |
1004
+ | --- | --- |
1005
+ | `GET /session?code=` | Consumes the one-time code, sets the cookie and returns 200 `text/html` that refreshes to `/`; 401 for a missing, wrong, used or expired code. |
1006
+ | `GET /`, `/wizard.js`, `/wizard.css` | `index.html`, `wizard.js`, `wizard.css` from `assetsDir`; 404 when absent. |
1007
+ | `GET /api/plan` | Runs `collectPlan(channel)` server-side for `?channel=release` (also the default without a query) or `?channel=main`, stores the plan under a new opaque `planId` and returns a view model: `actions` (`id` plus a fixed English description), `blockers` (`code`, `tool` — a preflight tool key such as `node` or `gentleAi`, otherwise `unknown` — and guidance), `profileChange` (whether `pnpm setup` edits the shell profile or Windows user PATH, and the bin directory), `persistence` (which of node, npm and pnpm go under `$PNPM_HOME`), `sharedFolders` (Windows: `{ tools, description }` for reused tools whose folders another account can change, or `null`), `ready` and `channel`. 409 while installing. Any other query is 400. |
1008
+ | `POST /api/install` | Body exactly `{ "planId": string, "consent": true }` (400 otherwise, including extra keys or a plan). 409 `install-running` while an installation runs; 409 `already-completed` once an installation has a final outcome (one installation per wizard run; the outcome is never replaced); 409 `plan-changed` for a stale `planId` or when a fresh re-inventory differs from the stored plan. Otherwise 202, and the runner receives the server-stored plan with `consent: true`. |
1009
+ | `GET /api/progress?after=<seq>` | Entries `{ seq, step, status, reason }` after `seq` from a ring buffer of the last 200; values outside `[a-z][a-z0-9-]*` become `unknown`, and no other runner field is kept. Also `running` and the final `outcome` with fixed guidance. |
1010
+ | `POST /api/shutdown` | Body empty or `{}`. Closes the host (409 while installing). |
1011
+
1012
+ `guidance` (exported) holds fixed English text for every runner blocked
1013
+ reason, every failed step, every preflight blocker code and both successful
1014
+ outcomes, plus a generic fallback; a runner exception becomes a `failed`
1015
+ outcome with the fallback. A failed `shell-setup`, `persist-path`, `acquire-go`, `install-global`,
1016
+ `install-shell-main`, `build-gentle-ai-main` or `update-shell` outcome also passes the
1017
+ runner's `detail` through, bounded again (string only, control and bidi
1018
+ characters removed, at most 300 characters); the host drops it for any other
1019
+ step. When that detail names GitHub's rate limit (`rate limit`, or `GitHub API`
1020
+ together with `403`), the guidance becomes `guidance.setupRateLimit`: GitHub's
1021
+ anonymous API limit was reached on this network, wait up to an hour and run the
1022
+ installer again. For `install-global`, `install-shell-main`, `build-gentle-ai-main`
1023
+ and `update-shell`, the same match, or `main-commit-unavailable` together with
1024
+ `HTTP 403`, selects `guidance.githubRateLimit`, the same advice without naming
1025
+ `gentle-shell setup`. No token or credential is ever requested. For `persist-path`,
1026
+ a detail with `ERR_PNPM_UNKNOWN_SHELL` or `ERR_PNPM_UNSUPPORTED_SHELL` selects
1027
+ `guidance.persistPathShell`: open a regular terminal and run the installer
1028
+ again, or add `$PNPM_HOME/bin` to PATH manually. For `acquire-go`, a detail
1029
+ starting with `Conflicting Go destination: ` selects
1030
+ `guidance.goDestinationConflict`: a folder from an earlier, interrupted run is in
1031
+ the way and is never replaced; remove that folder and run the installer again.
1032
+ `scripts/installer-runner.mjs` exports the frozen
1033
+ arrays `blockedReasons` and `failedSteps`; a test requires guidance for exactly
1034
+ those entries, and the runner tests check that every reason and step their
1035
+ scenarios observe is listed.
1036
+
1037
+ ### Wizard UI
1038
+
1039
+ `assets/install-wizard/index.html`, `wizard.js` (an ES module) and
1040
+ `wizard.css` are the whole browser side. They load only from the host itself
1041
+ and follow its CSP: no inline script or style, no external fonts, images or
1042
+ requests, and server strings are inserted as text nodes only (never parsed as
1043
+ HTML).
1044
+
1045
+ | Screen | What the user sees |
1046
+ | --- | --- |
1047
+ | Check | Shown while `GET /api/progress?after=0` and `GET /api/plan` run. A running installation resumes the Install screen; a finished one shows its outcome instead of a new plan. |
1048
+ | Review | Each fixed action with its description, the `profileChange` and `persistence` disclosures (state as text: "Will change", "No change"), on Windows a **Tools in folders other accounts can change** notice when the plan records one ([reused tool folders](#reused-tool-folders)), a **Git not found** notice when Git is missing (never a blocker), one consent checkbox and **Install Gentle Shell** (for a setup recovery, "Finish setting up Gentle Shell" and **Complete setup**). Without the checkbox no request is sent; the error is announced and focus moves to the checkbox. Preflight blockers replace the plan with their guidance, **Check again** and **Close installer**. |
1049
+ | Install | Expected runner steps (`expectedSteps`) with a text status (Done, In progress, Pending, Failed, Blocked, Not run), a `<progress>` bar and a terminal-style log of `{ seq, step, status, reason }` entries. |
1050
+ | Done | `ready`: run `gentle-shell`. `terminal-action-required`: open a new terminal, then run `gentle-shell`. `blocked`/`failed`: the host's guidance, the reason or failed step, the steps that finished and the log. A failed step with a detail also shows it under **Last error from** its label: `gentle-shell setup`, `pnpm setup`, `the Go download`, `pnpm add -g` (`install-global`), `the Gentle Shell main install`, `the Gentle AI main build` or `the Gentle Shell update`, as one plain text node (backticks stay literal). Every outcome offers **Close installer** (`POST /api/shutdown`). |
1051
+
1052
+ Behavior worth knowing:
1053
+
1054
+ - `409 plan-changed` reloads the plan, clears consent and explains why;
1055
+ `409 install-running` and `already-completed` resume the current state; 401
1056
+ or 403 ends the session with instructions to run the installer again. When
1057
+ the install request itself fails on the network, the wizard checks
1058
+ `/api/progress` once: a running or finished installation is followed
1059
+ instead of an error, and the install request is never re-sent
1060
+ automatically.
1061
+ - Progress polling starts at 500 ms, backs off by 1.5× to 4 s while nothing
1062
+ changes, resets when entries arrive and stops on the final outcome. Five
1063
+ consecutive errors, or three answers that are neither running nor finished,
1064
+ stop polling and offer **Try again**. The client keeps the last 200 log
1065
+ entries, like the host.
1066
+ - Accessibility: landmarks, one `h1` per screen that receives focus on each
1067
+ step change (it is not interactive, so it shows no focus ring), a polite
1068
+ `role="status"` region for progress and the closed state, an assertive
1069
+ `role="alert"` region for errors, labelled controls, native buttons and
1070
+ checkbox (keyboard operable), visible `:focus-visible` rings on every
1071
+ control, text labels for every color state, `prefers-reduced-motion` and
1072
+ `forced-colors` support.
1073
+ - Visual language: the gentlemanprogramming.com dark palette (accent
1074
+ `#f095c8` on `#1a1218`) and its static hero glow behind the panel, with
1075
+ local font stacks only and a text wordmark; no logos, brand images or
1076
+ favicon are copied.
1077
+
1078
+ `wizard.js` exports its pure models (`planModel`, `progressModel`,
1079
+ `outcomeModel`, `expectedSteps`, `installBody`, `interpretInstall`,
1080
+ `nextPoll`), its renderers and `createWizard({ document, fetch, setTimeout,
1081
+ clearTimeout, clipboard? })`; it starts itself only inside a page.
1082
+ `tests/install-wizard.test.ts` drives the controller with a fake DOM (where
1083
+ `innerHTML` throws), fake fetch and fake timers, and serves the real assets
1084
+ through the real host.
1085
+
1086
+ ### Development preview
1087
+
1088
+ `scripts/install-wizard-preview.mjs` starts the **real** host with the real
1089
+ assets but a fake `collectPlan` and a fake runner, so it never probes the
1090
+ machine, runs a package manager, downloads or installs anything:
1091
+
1092
+ ```sh
1093
+ node scripts/install-wizard-preview.mjs --scenario=terminal --step-ms=600
1094
+ ```
1095
+
1096
+ Scenarios: `ready`, `terminal` (`terminal-action-required` with runtime
1097
+ persistence), `blocked` (`existing-stack`), `failed` (`install-global`),
1098
+ `plan-changed` (the first plan goes stale, so the host answers 409),
1099
+ `recovery` (an installed stack whose setup did not finish; ends in
1100
+ `terminal-action-required`) and `preflight` (blockers, no plan). It prints the one-time session URL; Ctrl+C
1101
+ or **Close installer** stops it. The preview is a development tool: it is not
1102
+ listed in `installerPaths` (the entry never imports it), although it ships
1103
+ with the rest of `scripts/` in the package.
1104
+
1105
+ ### Remaining T6/T7 checks
1106
+
1107
+ - T6 (done): `wizard.js` and `wizard.css` are in `installerPaths`; the preview
1108
+ scenarios ran end to end in headless Chromium 1148 at 1440 and 390 px wide,
1109
+ including a keyboard-only consent and install, with no CSP violations.
1110
+ Screen readers and other browsers were not tested.
1111
+ - T7: the opener on each desktop (including a missing `xdg-open`); that the
1112
+ redirect file opens in the browser rather than another `.html` handler, and
1113
+ that sandboxed browsers with a private `/tmp` (for example snap or flatpak
1114
+ packages on Linux) can read it; that a meta refresh from a `file:` page
1115
+ reaches the session and the session page's same-origin refresh to `/`
1116
+ carries the `SameSite=Strict` cookie in each browser; browsers honouring
1117
+ `SameSite=Strict` and the cookie on `127.0.0.1`; DNS-rebinding and
1118
+ cross-site request rejection in real browsers; the bootstrap's handling of
1119
+ exit 1 end to end, and exit 0 outside Linux; SIGTERM during a running
1120
+ installation; Windows `rundll32` behavior; real probe and runner execution
1121
+ through the host outside Linux.
1122
+
1123
+ ## POSIX bootstrap: bundle-local tooling only
1124
+
1125
+ Run `sh scripts/bootstrap.sh` from a trusted extracted installation bundle or
1126
+ checkout. There is **no published bundle URL or remote-pipe installer contract**.
1127
+ The bundle must include `package.json`, both bootstrap modules and the wizard
1128
+ entry `bin/gentle-shell-install.mjs`. When that entry is missing, the script
1129
+ reports it before downloads or home writes. T7 owns packaging and distribution
1130
+ proof.
1131
+
1132
+ With that entry available, the fixed sequence is:
1133
+
1134
+ 1. Probe existing Node against the bundle's repository requirement. Unknown or
1135
+ prerelease versions block. A stable older version is never replaced: it is
1136
+ left as it is and the verified Node is acquired as in step 2.
1137
+ 2. If missing (or older), select a fixed native Node archive, download with TLS and bounded
1138
+ size/time, verify its hardcoded SHA256 using stock shell utilities, extract
1139
+ only its regular `bin/node`, and check the exact executable version before
1140
+ publishing it. Neither npm nor Corepack is acquired or invoked.
1141
+ 3. Reuse pnpm only after stable version, package engine and read-only global
1142
+ `add`/`bin` help-capability evidence. Engines support only simple `>=x.y` or
1143
+ `>=x.y.z` lower bounds; comparison fills an omitted patch with zero. Actual
1144
+ Node versions must remain exact stable versions; other ranges block rather
1145
+ than guess. The package is found from the resolved `pnpm` command; when that
1146
+ command is a regular cmd-shim file (as pnpm writes in `$PNPM_HOME` or
1147
+ `$PNPM_HOME/bin` when it installs or updates itself), the search starts from
1148
+ its `# cmd-shim-target=` path or, without one, the single
1149
+ `"$basedir/<target>" "$@"` it runs. A shim is a script (`#!`) read whole,
1150
+ up to 64 KiB, so a target named after the first 4 KiB is still found; a
1151
+ larger script fails closed. That path only locates the evidence;
1152
+ `pnpm --version` must still match it. A standalone pnpm (a Mach-O or ELF
1153
+ executable, directly or as that shim target, as mise, asdf and pnpm's own
1154
+ installer with `@pnpm/exe` provide) embeds its Node runtime: it skips the
1155
+ package engine check but must still report an exact stable `--version`, equal
1156
+ to the version of the `pnpm` or `@pnpm/*` `package.json` beside it when there
1157
+ is one, and pass the same global `add`/`bin` capability checks. A script or
1158
+ shim without a `pnpm` package still blocks. Prerequisite
1159
+ checks run from `/`, so a pnpm that switches to a project's `packageManager`
1160
+ pin reports its own version.
1161
+ A pnpm whose stable version evidence (`package.json`, or a standalone
1162
+ pnpm's `--version`) is of another major or older than 11.1.1, or whose
1163
+ simple engine bound rejects this Node, is left as it is, and pnpm is acquired
1164
+ as when missing; the private tools directory is claimed only then.
1165
+ Missing pnpm is acquired from a fixed registry tarball, SHA512-SRI verified,
1166
+ checked for unsafe paths/links, extracted and probed before publication.
1167
+ 4. Start the fixed bundle entry with the refreshed child environment. A mandatory
1168
+ acquisition/probe/child failure is an error, never installation success.
1169
+
1170
+ ### Ownership and failure boundaries
1171
+
1172
+ Acquisition uses a mode-0700, uniquely created
1173
+ `$HOME/.gentle-shell-bootstrap-tools.<random>` directory, with an explicit
1174
+ `.bootstrap-owned` marker. This is **prerequisite tooling, not a product home**.
1175
+ HOME must be absolute, owned by the current user, not group/world writable and
1176
+ free of symlink ancestors. Staging and destinations reject conflicts and
1177
+ symlinks. Unrelated similarly named directories are not scanned, reused or
1178
+ removed. Failed attempts clean only their own private directories; successful
1179
+ acquisition removes its temporary archives/staging and retains verified tools
1180
+ while the wizard runs. After the wizard exits 0, the script removes the tools
1181
+ directory it claimed, and the Node helper removes the one it created when only
1182
+ pnpm was missing. Both removals require the exact
1183
+ `$HOME/.gentle-shell-bootstrap-tools.*` name, a real directory (not a symlink)
1184
+ and the `.bootstrap-owned` marker with its exact content; `rm -rf` and
1185
+ `rmSync` remove links inside without following them. When removal is refused or
1186
+ fails, the bootstrap still exits 0 and prints
1187
+ `Bootstrap: installation finished, but temporary tools could not be removed:
1188
+ <path>` on stderr.
1189
+ A new attempt reuses tools only if already visible and proven on its PATH; it
1190
+ does not discover or garbage-collect previous private attempts.
1191
+
1192
+ No profile, global PATH, existing executable installation, product agent home,
1193
+ Engram state or companion installation is changed. Added paths affect only the
1194
+ bootstrap and its child. T4 must implement standard global installation and the
1195
+ persistent **ordinary terminal** handoff; these private wrappers are not that
1196
+ handoff. Existing unverifiable pnpm wrappers/binaries block instead of being
1197
+ silently replaced. No sudo or security exclusions are requested.
1198
+
1199
+ Stock utilities are prerequisites, not silently installed: POSIX sh, awk,
1200
+ dirname, uname, mkdir, mktemp, chmod, mv, rm, sleep, wc, id and ls; missing Node
1201
+ also needs curl, tar and sha256sum or shasum. Linux acquisition additionally
1202
+ requires getconf evidence of glibc >=2.28. Missing pnpm requires tar. Missing
1203
+ utilities are reported. Shell executable-version probes have a 10-second
1204
+ watchdog, hash/archive probes 30 seconds; curl has a 10-second connection and
1205
+ 120-second total limit with a 100-MiB artifact cap. A subprocess-only `ulimit -f`
1206
+ adds a hard disk ceiling (at most 200 MiB depending on shell block units) for
1207
+ older curl implementations; failure to establish it blocks acquisition.
1208
+ The JavaScript transport rejects
1209
+ redirects, caps pnpm at 32 MiB and aborts after 60 seconds; process checks have
1210
+ 15-second/1-MiB bounds. At a prerequisite-process deadline, the shell watchdog
1211
+ and Node process adapter send SIGKILL to their directly spawned child, rather
1212
+ than catchable SIGTERM. A killed probe is a failure even if it printed valid
1213
+ output before hanging. Shell watchdog cancellation also kills and waits for its
1214
+ owned sleeper. The interactive wizard child intentionally has no total runtime
1215
+ timeout. Raw downloader/process error text is not logged.
1216
+
1217
+ ### Artifact trust and shared helper API
1218
+
1219
+ | Artifact | Acquisition pin and provenance |
1220
+ | --- | --- |
1221
+ | Node native darwin/linux x64/arm64 | 24.21.0; parent-verified SHA256 entries from [official SHASUMS256](https://nodejs.org/dist/latest-v24.x/SHASUMS256.txt), applied to fixed [v24.21.0 archive URLs](https://nodejs.org/dist/v24.21.0/). |
1222
+ | pnpm JavaScript CLI | 11.1.1; parent-verified raw engine `>=22.13` (comparison minimum `22.13.0`), tarball and SHA512 SRI from [registry version metadata](https://registry.npmjs.org/pnpm/11.1.1); [fixed tarball](https://registry.npmjs.org/pnpm/-/pnpm-11.1.1.tgz). |
1223
+
1224
+ POSIX/pnpm integrity values live in `installer-downloads.mjs`; Windows Node
1225
+ values live in `installer-windows-artifacts.json`. The shell copies
1226
+ only Node's acquisition pin/hashes because first acquisition cannot depend on
1227
+ Node. Tests cross-check shell selection against the shared descriptors. Changing
1228
+ these pins requires renewed primary-source integrity evidence and updating both
1229
+ Node representations together. There is no live latest resolution. The pnpm pin retains the exact raw upstream
1230
+ engine string for metadata identity checks; only version comparisons normalize
1231
+ its supported partial minimum. Product versions/minima remain repository-derived;
1232
+ the pnpm pin is not a compatibility
1233
+ minimum. Registry-hosted artifacts do not imply using the npm executable.
1234
+
1235
+ `scripts/installer-downloads.mjs` exports `artifactFor(name, platform, arch)`
1236
+ (frozen allowlisted descriptors), `verifiedDownload(name, adapters, platform,
1237
+ arch)` (verified bytes), `compatibleEngine(range, version)`,
1238
+ `ensurePnpm({ tools, env, nodeVersion, adapters })` and
1239
+ `launchWizard({ bundle, env })`. Only known artifact names are accepted, not
1240
+ caller URLs/checksums/commands. Trusted local test adapters inject byte download,
1241
+ digest and process checks; they are not exposed through any browser interface.
1242
+ The caller owns a private `tools` directory. `ensurePnpm` returns `{ env,
1243
+ acquired }`, leaving the supplied environment unchanged. The Windows adapter reuses descriptors/integrity verification without changing
1244
+ POSIX process semantics, defaults or pins.
1245
+
1246
+ ### Evidence, not platform certification
1247
+
1248
+ Native shell execution was exercised on this Linux host with disposable Unicode
1249
+ and whitespace paths, fake OS/download/hash/archive utilities and injected
1250
+ JavaScript acquisition/process adapters. No real network or host prerequisite
1251
+ installation was performed. Darwin selections are simulated, **not native
1252
+ macOS execution**. Native macOS, Linux clean-machine/loader and minimum macOS
1253
+ version acceptance remain T7 work. Linux acquisition is native glibc-only:
1254
+ musl/Alpine and emulation/Rosetta compatibility are not claimed. Node's
1255
+ [upstream build/platform requirements](https://github.com/nodejs/node/blob/v24.21.0/BUILDING.md)
1256
+ remain the platform reference; successful descriptor planning alone proves no
1257
+ OS/kernel/libc support.
1258
+
1259
+ ### Subprocess deadline evidence
1260
+
1261
+ The regression fixtures exercise production process paths without shortened
1262
+ production timeouts or a mocked process adapter:
1263
+
1264
+ | Path | Observed Linux fixture behavior |
1265
+ | --- | --- |
1266
+ | Shell acquired-Node version probe | A single Node process prints the expected version, ignores TERM and waits without busy-looping. The real 10-second watchdog kills/reaps it, rejects acquisition and removes owned tooling while preserving an unrelated HOME file. |
1267
+ | Node helper tar probe | Fake verified bytes feed `ensurePnpm`, but its default process adapter runs a real single-process TERM-ignoring tar stand-in. The real 15-second deadline kills/reaps it, rejects acquisition and removes owned staging while preserving an unrelated file. Ordinary nonzero tar exit also rejects and cleans staging. |
1268
+
1269
+ Before correction, both paths exceeded their deadlines and needed the test
1270
+ harness's independent outer SIGKILL guard (13 seconds for shell, 18 for Node).
1271
+ The guard kills only each freshly created detached fixture group and also
1272
+ cleans residual fixture processes on exit. Production does **not** kill groups,
1273
+ match process names or use an external timeout utility. After correction, both
1274
+ probe PIDs are reaped before their parents return; neither outer guard fires.
1275
+ Ordinary success remains covered by the existing reuse/acquisition tests.
1276
+
1277
+ These are **direct-child, non-forking probe** guarantees, not process-tree
1278
+ cancellation evidence. Unknown programs that fork descendants retaining stdio
1279
+ may keep pipes open; this unit does not claim bounded return or descendant
1280
+ cleanup for those programs. The fixtures prove neither native macOS behavior
1281
+ nor live artifact acquisition. Independent verification and native review remain
1282
+ separate parent-owned gates.
1283
+
1284
+ ## Windows foundation: fixed commands, no policy repair
1285
+
1286
+ Run `scripts\bootstrap.cmd` from a trusted extracted bundle or checkout. Like
1287
+ POSIX, it stops before acquisition or home writes when the wizard entry
1288
+ `bin/gentle-shell-install.mjs` or any Windows-dependent helper/metadata file is
1289
+ missing. There is no published bundle URL or remote-pipe contract, and the
1290
+ Windows bootstrap end to end still lacks native acceptance evidence.
1291
+
1292
+ | Step | Windows contract |
1293
+ | --- | --- |
1294
+ | Entry | Small CMD entry invokes fixed stock Windows PowerShell commands with no profile. Paths are environment data, not interpolated PowerShell source. Delayed CMD expansion is disabled. |
1295
+ | Storage | Claim a new random-named prerequisite directory directly below `%LOCALAPPDATA%`, or below `%USERPROFILE%` when `%LOCALAPPDATA%` fails `acl-mask` or `home-owner` (see [claim candidates](#claim-candidates-and-owners)); never reuse an existing destination. Verify each path component's reparse attributes, owner and role-specific ACL rights. Set the claimed directory's owner to the invoking SID, protect its DACL for the invoking SID, SYSTEM and Administrators, and read both back. |
1296
+ | Node | Reuse a proven stable existing Node ≥24.3.0 and the repository minimum. Otherwise (missing, an older stable version, or a Node whose storage fails the reparse/owner/ACL walk, which therefore never runs) leave it as it is and acquire only the fixed official Node 24.21.0 Windows x64/arm64 ZIP, with no redirects and bounded transport, verify SHA256 before opening the archive, validate the whole namespace and extract only regular `node.exe`. |
1297
+ | pnpm | A pnpm whose wrapper, Node, entry, metadata or exe fails the reparse/owner/ACL walk never runs: it is left as it is and pnpm is acquired as when missing. Reuse only a [known shim](#windows-command-shims) of pnpm's own JS entry, with package identity, bin target, stable CLI version, compatible engine and global add/bin help evidence, preserving its sibling-Node preference or proving its inherited cwd/PATH/PATHEXT Node selection; or a native `pnpm.exe` (`pnpm setup`, `pnpm self-update`, @pnpm/exe, pnpm 12), directly or as a known shim's target (an extensionless target resolves through PATHEXT in order, and only an `.exe` first match counts), whose `--version` is stable and equals the `package.json` naming pnpm or `@pnpm/*` beside it, with the same global help evidence. A `pnpm.exe` without that `package.json` (pnpm's standalone installer, a Volta or mise shim), Corepack's `pnpm.cmd` and mise's file shim are not proven pnpm: they are never run and pnpm is acquired as when missing. Never execute a shim via cmd.exe. Other unknown wrappers block without replacement. One whose stable version is of another major or older than 11.1.1 is left as it is, and pnpm is acquired as when missing. |
1298
+ | Missing pnpm | Shared pnpm 11.1.1 URL/SRI and raw `>=22.13` engine identity are unchanged. Parse bounded gzip/USTAR bytes, reject unsupported extensions, links and unsafe Windows namespaces before no-clobber publication. Return a direct Node + JS-entry invocation; do not fabricate a wrapper. |
1299
+ | Handoff | Existing Node helper starts the fixed wizard entry `bin/gentle-shell-install.mjs`. Only child PATH is refreshed. No persistent PATH, global installation, product root or companion installation is created here. |
1300
+
1301
+ Windows Node SHA256 provenance is the parent's fresh primary-source read of
1302
+ [24.21.0 SHASUMS256](https://nodejs.org/dist/v24.21.0/SHASUMS256.txt). A narrow
1303
+ JSON metadata file lets pre-Node PowerShell and the shared Node helper consume
1304
+ the same Windows pins without mixing the POSIX archive format or copying pnpm
1305
+ integrity into another runtime. Pin changes still require fresh primary evidence.
1306
+
1307
+ ### T4/T5 integration contract
1308
+
1309
+ `scripts/installer-windows.mjs` exports:
1310
+
1311
+ - `ensureWindowsPnpm({ tools, env, node?, adapters? })` → `{ acquired, env,
1312
+ command, prefix }`. Invoke `[command, ...prefix, ...args]` with `shell:false`;
1313
+ keep the returned environment. `tools` must be a private attempt-owned root.
1314
+ Test adapters are trusted local code only, never a browser API.
1315
+ - `bootstrapWindows({ bundle, tools, env })` validates fixed bundle files and
1316
+ requirements, acquires/reuses pnpm, then delegates to shared `launchWizard`.
1317
+ CLI entry is `installer-downloads.mjs --bootstrap-windows BUNDLE TOOLS`.
1318
+ - T5 receives `GENTLE_INSTALL_PNPM_NODE` and `GENTLE_INSTALL_PNPM_ENTRY`, or
1319
+ `GENTLE_INSTALL_PNPM_COMMAND` alone for a native `pnpm.exe`, as data for that
1320
+ proven direct invocation. T4 must re-inventory before installation;
1321
+ these values are not an arbitrary command API or persistent terminal repair.
1322
+ - Storage, namespace, wrapper, tar and bounded process functions are exported
1323
+ for focused verification. Production storage verification requires native
1324
+ Windows and fixed stock PowerShell ACL commands, never POSIX mode/UID evidence.
1325
+
1326
+ Go preparation is not part of the Windows bootstrap. The standard installation
1327
+ runner blocks on Windows unless preflight reports a reusable Go ≥1.25.10 or the
1328
+ plan acquires the [pinned Go](#pinned-go) after consent, because gentle-pi's
1329
+ postinstall may build Gentle AI from source; it never treats an explicit
1330
+ override as native-package evidence.
1331
+
1332
+ That Go SumDB source build runs Go in a fresh private directory created with
1333
+ `mkdtemp` under the system temp directory (`%TEMP%\gai-XXXXXX`: GOBIN, GOPATH,
1334
+ GOMODCACHE, GOCACHE, TEMP/TMP and the commands' working directory), never inside
1335
+ the installed package, and always removes it. The binary is then copied into the
1336
+ package's staging directory, checked there (build metadata, exact version,
1337
+ SHA-256 manifest) and published as before. Run 38063142924 (LongPathsEnabled=0)
1338
+ reproduced why: from the 179-character pnpm 11 store path of a main package, the
1339
+ build inside the package failed with `asm.exe: fork/exec …: The directory name is
1340
+ invalid.`, because the working directories Go gives `asm.exe` inside GOMODCACHE
1341
+ exceed MAX_PATH, which CreateProcess rejects; the same build succeeded from a
1342
+ 4-character and a 161-character root. When the build fails, the postinstall prints
1343
+ the whole `cause` chain with each code and the last 4000 characters of the failed
1344
+ command's stderr and stdout.
1345
+
1346
+ ### Policy, bounds and evidence limits
1347
+
1348
+ No unsigned `.ps1` file, script-file evaluation, execution-policy relaxation,
1349
+ file unblocking, certificate/TLS bypass, elevation, security exclusion or profile
1350
+ change is used. Restricted permits individual commands; it is not every Windows
1351
+ client's default. [Execution policies](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies)
1352
+ do not override [AppLocker CMD/BAT rules](https://learn.microsoft.com/en-us/windows/security/application-security/application-control/app-control-for-business/applocker/script-rules-in-applocker)
1353
+ or [managed App Control constraints](https://learn.microsoft.com/en-us/powershell/scripting/security/app-control/how-app-control-works).
1354
+ ConstrainedLanguage and any download/execution denial stop with an error. There
1355
+ is no fallback intended to evade those controls; if CMD itself is denied, the
1356
+ OS owns the denial diagnostic before our entry can run.
1357
+
1358
+ Node transport is capped at 100 MiB with a 60-second request/total elapsed check
1359
+ and 10-second blocking-read timeout. A blocking read can consume up to that last
1360
+ read timeout beyond the elapsed check. ZIP metadata caps expanded size at 512 MiB
1361
+ and the selected executable at 150 MiB. Namespace checks reject traversal,
1362
+ backslashes, links/reparse attributes, duplicate/case-aliased names, conflicting
1363
+ file/directory parents, devices, ADS and trailing dots/spaces. Shared pnpm
1364
+ transport remains 32 MiB/60 seconds; tar expansion is capped at 128 MiB.
1365
+ Unsupported archive extensions fail closed. **Actual pnpm 11.1.1 TGZ acceptance
1366
+ is unproved:** it requires an integrity-backed offline copy of the pinned
1367
+ artifact, not a synthetic USTAR fixture or an injected digest. No such evidence
1368
+ was supplied and no acquisition is authorized in this correction.
1369
+
1370
+ The pre-Node probe drains both pipes with a combined 1-MiB cap and kills only
1371
+ its direct child at 10 seconds, with a bounded one-second final wait. Node helper
1372
+ probes use the shared 15-second/1-MiB-style bounds and an unignorable direct-child
1373
+ kill. Valid output followed by a hang is a failure. Production never kills
1374
+ process groups or unrelated processes. Forked descendants retaining stdio are
1375
+ outside the guarantee; the interactive wizard has no total deadline.
1376
+
1377
+ ### Claim candidates and owners
1378
+
1379
+ The claim tries at most two bases, in this order:
1380
+
1381
+ 1. `%LOCALAPPDATA%`, with the CMD's random `.gentle-shell-bootstrap-tools.*` name.
1382
+ 2. `%USERPROFILE%`, with its own random name. It is tried only when the first
1383
+ candidate fails `acl-mask` or `home-owner`. Any other rejection stops at once.
1384
+
1385
+ A beta tester's profile showed why. Another principal had Modify rights
1386
+ (`0x1301bf`) on `%LOCALAPPDATA%` and on `AppData`, so the claim failed
1387
+ `acl-mask`. That rejection is right: a folder another principal can write is
1388
+ never used to run binaries, and it is never relaxed. `%USERPROFILE%` does not
1389
+ sit below `AppData`, so it can still be a clean base.
1390
+
1391
+ Each candidate gets every check: the exact-parent rule (`path-mismatch`), the
1392
+ base owner (`home-owner`) and the whole ancestor walk up to the drive root
1393
+ (`ancestor-reparse`, `ancestor-owner`, `acl-mask`). Nothing is created until one
1394
+ candidate passes them all. The `acl-mask` masks, the reparse checks, the
1395
+ collision check, the random name and the no-UNC rule are the same for both.
1396
+ When the fallback is claimed, the stage exits 2 and CMD switches
1397
+ `GENTLE_BOOTSTRAP_TOOLS` to it before it records ownership. Every later stage,
1398
+ the failure cleanup and the success cleanup use that one path.
1399
+
1400
+ The base may be owned by the invoking SID, SYSTEM (`S-1-5-18`) or
1401
+ BUILTIN\Administrators (`S-1-5-32-544`). The tester's profile was owned by
1402
+ SYSTEM. Both principals are already trusted for every ancestor and keep
1403
+ FullControl in the private DACL, so neither gains anything new.
1404
+ TrustedInstaller, which ancestors may still have as owner, and any other SID
1405
+ fail `home-owner`.
1406
+
1407
+ An Administrators member's new folders may default to BUILTIN\Administrators as
1408
+ owner. The claim never relies on that default. It writes the invoking SID as
1409
+ owner together with the protected DACL, then reads back the protection, the
1410
+ owner (`private-owner`) and every ACE.
1411
+
1412
+ When no candidate passes, the first line is unchanged and still ends with
1413
+ `Reason: <code>`, the code of the check that stopped the claim. One more line
1414
+ follows for each candidate tried, with its folder and code. For `acl-mask` the
1415
+ line also names the folder holding the ACE, the principal (SID, and the account
1416
+ name when it resolves) and the rights it was allowed. For `home-owner` it names
1417
+ the owner. For example:
1418
+
1419
+ ```text
1420
+ Bootstrap: private storage ACL/reparse/ownership claim failed or policy denied it. Reason: acl-mask
1421
+ Bootstrap: storage candidate C:\Users\me\AppData\Local: acl-mask at C:\Users\me\AppData\Local: S-1-5-21-…-1002 (PC\other) allowed 0x001301BF
1422
+ Bootstrap: storage candidate C:\Users\me: acl-mask at C:\Users: S-1-5-21-…-1002 (PC\other) allowed 0x00100002
1423
+ ```
1424
+
1425
+ A failure after the walk (`collision`, `protected-dacl`, `private-owner`,
1426
+ `private-ace`, or an unexpected step) adds a line naming the chosen directory.
1427
+
1428
+ The Node helper accepts the same location. Its storage check walks the tools
1429
+ directory under `%USERPROFILE%` with the same masks and owners, so the
1430
+ tester's layout passes and a writable ancestor still rejects it.
1431
+
1432
+ The helper also stops failing on the user's own pnpm in such a profile. A pnpm
1433
+ whose wrapper, Node, entry, metadata or `pnpm.exe` fails the reparse, owner or
1434
+ ACL walk is never run: like an untrusted user Node in `bootstrap.cmd`, it is
1435
+ left as it is, and the pinned pnpm is acquired into the private directory. So
1436
+ is a `pnpm.exe` without pnpm's `package.json` beside it, Corepack's `pnpm.cmd`
1437
+ and mise's file shim: nothing proves they are pnpm, so they count as no pnpm
1438
+ at all. A `policy` denial, unknown evidence and every other check still stop.
1439
+ In the wizard, the user's pnpm in `$PNPM_HOME\bin` that fails the walk does not
1440
+ block either: it is not run, not replaced and not downgraded, and the
1441
+ bootstrap's verified pnpm runs every pnpm step.
1442
+
1443
+ ### Windows PNPM_HOME
1444
+
1445
+ The bootstrap's private directory only holds the temporary tools. The wizard
1446
+ then installs Node.js, npm, pnpm, Pi and Gentle Shell under PNPM_HOME and runs
1447
+ them from `$PNPM_HOME\bin`. On the tester's profile that folder,
1448
+ `%LOCALAPPDATA%\pnpm`, sat below the same `%LOCALAPPDATA%` another account
1449
+ could modify. So before any probe the wizard walks PNPM_HOME with the same
1450
+ checks as the tools directory: reparse points, trusted owners and the
1451
+ `acl-mask` masks, up to the drive root. It walks the folder and its `bin` when
1452
+ they exist, or the nearest existing ancestor when the folder does not exist
1453
+ yet (held to the strict depth-0 mask).
1454
+
1455
+ | PNPM_HOME | Walk | Result |
1456
+ |-----------|------|--------|
1457
+ | Set by the user | Passes | Used as before. |
1458
+ | Set by the user | Fails | Blocked: `untrusted-pnpm-home`. |
1459
+ | Not set: `%LOCALAPPDATA%\pnpm` | Passes | Used as before; the plan is unchanged. |
1460
+ | Not set: `%LOCALAPPDATA%\pnpm` | Fails, and nothing is installed there | `%USERPROFILE%\.pnpm` is used, if it passes the walk too. |
1461
+ | Not set: `%LOCALAPPDATA%\pnpm` | Fails, and something is installed there | Blocked: `untrusted-pnpm-home`. |
1462
+ | Any | The walk cannot finish (policy, PowerShell) | Blocked: `unknown-tool` for `pnpmHome`. |
1463
+
1464
+ A blocked decision (`untrusted-pnpm-home`, or `unknown-tool` for `pnpmHome`)
1465
+ runs no probe at all: the node probe would otherwise run the user's npm with
1466
+ that PNPM_HOME's `bin` first on PATH. The plan then holds only that blocker.
1467
+
1468
+ "Nothing is installed" has one exact meaning: `%LOCALAPPDATA%\pnpm` does not
1469
+ exist, or it is a real directory (not a link) with no entries at all. Any
1470
+ entry, even an empty `store` folder or a pnpm config file, counts as an
1471
+ installation. The installer never moves, deletes or adopts it.
1472
+
1473
+ `%USERPROFILE%\.pnpm` is accepted only when it passes the walk and is absent,
1474
+ empty, or marked as created by this flow: it holds a regular
1475
+ `.gentle-shell-pnpm-home` file with the exact text
1476
+ `gentle-pi private pnpm home`. A non-empty folder without that marker is
1477
+ foreign and blocks, and so does one that fails the walk.
1478
+
1479
+ The plan says all of this before consent: the default folder, the account
1480
+ (SID, and its name when it resolves) and the rights that failed, the private
1481
+ folder, and that `pnpm setup` will save `PNPM_HOME=%USERPROFILE%\.pnpm` and
1482
+ add `%USERPROFILE%\.pnpm\bin` to the user PATH for new terminals. A blocker
1483
+ names the same folder, account and rights, plus the remedy: remove that
1484
+ account's write access, or set PNPM_HOME to a private folder such as
1485
+ `%USERPROFILE%\.pnpm` and reinstall the global pnpm packages there.
1486
+
1487
+ After consent the runner's first step, `prepare-pnpm-home`, claims the private
1488
+ folder before any pnpm command could create it with inherited permissions. It
1489
+ uses the bootstrap claim's exact statements: owner set to the user, a
1490
+ protected DACL with FullControl only for the user, SYSTEM and Administrators,
1491
+ then a readback of the protection, the owner and every ACE. An empty existing
1492
+ folder gets the same DACL; a marked one is kept as it is. Then the marker is
1493
+ written, a `tmp` folder is created inside it (a real directory, never a link),
1494
+ and the folder and `tmp` are walked again. Any failure stops the installation
1495
+ before any command (`prepare-pnpm-home`).
1496
+
1497
+ The installer's children, and an update's `gentle-shell upgrade` children, get
1498
+ `TEMP` and `TMP` set to that `tmp` folder. It inherits the protected DACL, so
1499
+ a postinstall's `os.tmpdir()` stays private: the Windows Gentle AI source
1500
+ build creates its `gai-` directory there and runs `gentle-ai.exe` from it
1501
+ before publishing it. A default `%TEMP%` lives under `%LOCALAPPDATA%`, which
1502
+ another account may write. These values are never persisted, and nothing
1503
+ changes outside private mode or on POSIX.
1504
+
1505
+ `pnpm setup` persists the folder. pnpm 11.1.1's `setup` reads its home from
1506
+ the `PNPM_HOME` environment variable (`getDataDir`) and, on Windows, writes it
1507
+ to `HKCU\Environment` as `PNPM_HOME`, then puts `%PNPM_HOME%\bin` first in the
1508
+ user Path (`addDirToWindowsEnvPath`). The runner gives `pnpm setup` the
1509
+ private PNPM_HOME, and runs it even for an update, which otherwise keeps the
1510
+ PATH its installation already uses. A registry `PNPM_HOME` that already holds
1511
+ another value makes `pnpm setup` fail with `ERR_PNPM_BAD_ENV_FOUND` instead of
1512
+ overwriting it. The native Windows tests check the decision, the claim and the
1513
+ probe. No test runs `pnpm setup` against a real user registry: the registry
1514
+ write is pnpm's own behavior, read from the pnpm 11.1.1 package source.
1515
+
1516
+ pnpm also keeps its global config (`%LOCALAPPDATA%\pnpm\config`), cache
1517
+ (`%LOCALAPPDATA%\pnpm-cache`) and state (`%LOCALAPPDATA%\pnpm-state`) under
1518
+ `%LOCALAPPDATA%`. For exact versions, pnpm 11.1.1 resolves from cached registry
1519
+ metadata without asking the registry. With a private PNPM_HOME, the installer's
1520
+ own children therefore get `XDG_CONFIG_HOME`, `XDG_CACHE_HOME` and
1521
+ `XDG_STATE_HOME` under it, unless the user set them. They are never persisted:
1522
+ `pnpm setup` still saves only PNPM_HOME and the user Path.
1523
+
1524
+ Residual risk (accepted, and stated in the plan before consent): after the
1525
+ installation, the user's own pnpm commands, and Gentle Shell's later
1526
+ `gentle-shell upgrade`, still use pnpm's default config, cache and state
1527
+ folders under `%LOCALAPPDATA%` unless the user configures them. pnpm 11.1.1
1528
+ resolves its config folder only from `XDG_CONFIG_HOME` or
1529
+ `%LOCALAPPDATA%\pnpm\config`, so there is no safe persistent pnpm-native
1530
+ setting the installer could save instead. The installer protects its own run
1531
+ and the binaries it persists; it does not make the rest of a writable
1532
+ `%LOCALAPPDATA%` safe.
1533
+
1534
+ Accepted risk (user decision): the installer never creates or runs its own
1535
+ binaries in a folder another account can change. The user's own tools are
1536
+ used the way the user already uses them: a Node.js, npm, Go, Pi or npm-owned
1537
+ Gentle Shell that the plan reuses or updates may live in such a folder (an
1538
+ npm global prefix under `%APPDATA%`, for example), and the installer runs it
1539
+ there, as any terminal would. Only pnpm, the installer's own runner, and the
1540
+ Node.js that `bootstrap.cmd` runs are held to the walk as a blocker. The plan
1541
+ discloses the rest as a notice (below).
1542
+
1543
+ #### Reused tool folders
1544
+
1545
+ Before consent the wizard walks every reused tool's command and what it runs:
1546
+ `node`, `npm`, `go`, `pi` and `gentle-shell` as the user's PATH resolves them,
1547
+ plus the Node and JS entry of a known shim. Each file is walked where it really
1548
+ is (its realpath), and for every link on its path (a symbolic link or a
1549
+ junction, as `lstat` reports it) the real folder that holds the link is walked
1550
+ too, since whoever can change that folder can retarget the link. A component
1551
+ whose realpath is merely spelled differently, such as an 8.3 short name
1552
+ (`C:\Users\RUNNER~1`), is not a link: walking `C:\Users` as a link holder would
1553
+ hold `C:\` to the strict parent mask, which Windows' default
1554
+ CreateDirectories (`0x4`) for Authenticated Users fails. A real location that
1555
+ is not on a local drive (a mapped network drive, or a link to a UNC share) is
1556
+ never sent to PowerShell: it is "could not be checked" for that tool only, and
1557
+ the rest is walked as usual. pnpm's own links, the global `global\v11\<hash>\node_modules\<pkg>`
1558
+ junction into the store and a `pnpm runtime` `node.exe`, are therefore never a
1559
+ finding, while a weak ACL on the folders they lead to, or on the folder holding
1560
+ them, still is. A file whose real location cannot be read is reported as
1561
+ "could not be checked". All of them go to one Windows
1562
+ PowerShell launch (`verifyWindowsStorageMany`): the paths travel as one
1563
+ environment value joined by `|`, which no Windows path holds, and the script
1564
+ prints one line per path, in order: `safe`, the same `unsafe:<role>-<check>|<detail>`
1565
+ as the single-path walk, or `unknown` when that path's walk fails for another
1566
+ reason (an owner that denies READ_CONTROL, for example). `unknown` becomes
1567
+ `{ check: "unchecked", at }`: the notice says that path could not be checked.
1568
+ A count that does not match, any other line, or `unsafe:policy` rejects
1569
+ the whole result. A test keeps that walk identical to the single-path one.
1570
+
1571
+ The plan records only the tools it reuses as they are: the user's Node.js and
1572
+ its npm, a Go that a build reuses, a Pi kept as it is, and a Gentle Shell
1573
+ that npm owns (kept or updated with npm). `tools.folders` is then
1574
+ `{ status: "notice", reused: [{ tool, check, at, sid?, account?, rights? }] }`
1575
+ (per tool the first walk rejection, else the first `unchecked` path), and the
1576
+ review screen names each tool, folder, account and rights. It never
1577
+ blocks and changes no action: the user decided to accept this risk. A
1578
+ policy denial, a PowerShell failure or a walk that cannot finish means no
1579
+ notice, never a blocker. The notice is left out of the plan fingerprint the
1580
+ install request re-checks, so a re-inventory whose walk timed out, or found a
1581
+ different notice, still installs the consented plan instead of answering
1582
+ `plan-changed` again and again.
1583
+
1584
+ A pnpm whose storage fails the walk is never run anywhere. That includes the
1585
+ wizard probe's `--version` of the user's own pnpm next to the bootstrap's:
1586
+ its shim and what the shim runs are walked first, and any failure counts as
1587
+ not usable.
1588
+
1589
+ ### Target-versus-ancestor ACL boundary
1590
+
1591
+ For untrusted SIDs, the fixed predicates allow ReadAndExecute plus Synchronize
1592
+ (`0x1200a9`), with one narrow directory-specific distinction:
1593
+
1594
+ | Role | Mutation boundary |
1595
+ | --- | --- |
1596
+ | Actual file/executable or owned tooling root (depth 0) | No write, deletion, child-deletion, ACL or ownership rights. |
1597
+ | Immediate parent (depth 1) | Same strict protection, including no CreateDirectories/AppendData. A new tooling claim starts checking its parent at this depth. |
1598
+ | Distant **existing directory** (depth ≥2) | May additionally allow CreateDirectories/AppendData (`4`) for sibling-only creation. WriteData/CreateFiles, WriteExtendedAttributes, WriteAttributes, Delete, DeleteSubdirectoriesAndFiles, WriteDACL, TakeOwnership, generic/unknown rights still reject. |
1599
+
1600
+ Directory CreateDirectories and file AppendData share an enum bit; a distant
1601
+ ancestor here must actually be an existing directory. This is not permission to
1602
+ append to an executable. Reparse checks, trusted ownership and every actual path
1603
+ component remain mandatory. An inheritable ACE is checked again where it becomes
1604
+ effective on the descendant; InheritOnly does not grant rights on its current
1605
+ object. Effective unsafe allow ACEs block even when deny ACEs might otherwise
1606
+ limit them: this is a conservative mutation boundary, not a general Windows
1607
+ ACL/access-check framework. Trusted SIDs and protected-DACL readback are unchanged.
1608
+ The portable fixture model checks parity with all three fixed production
1609
+ predicates; it does not execute Windows ACL APIs or claim prevalence on Windows.
1610
+
1611
+ Failure cleanup targets only the directory claimed by the actual attempt;
1612
+ collisions and unrelated storage are not removed. Prerequisite tools remain
1613
+ available to children while the wizard runs. After the helper exits 0, a final
1614
+ stage removes the claimed root only when its full path is a direct child of
1615
+ `%LOCALAPPDATA%` or `%USERPROFILE%` named `.gentle-shell-bootstrap-tools.*`, neither it nor its
1616
+ `.bootstrap-owned` marker is a reparse point, and the marker holds the exact
1617
+ text. It uses `[IO.Directory]::Delete(path, true)`, which does not recurse
1618
+ through reparse points, instead of Windows PowerShell 5.1 `Remove-Item`. A
1619
+ refusal or failure prints the same notice with the path and still exits 0.
1620
+ Redirected cleanup notices use UTF-8 so captured logs preserve Unicode paths.
1621
+ Interactive stderr keeps its existing encoding; bootstrap does not change the
1622
+ console code page. PowerShell helper lookup honors Windows `SystemRoot` casing
1623
+ in copied environments. ACL/ancestor checks are conservative and may
1624
+ reject managed/nonstandard layouts rather than relax security. They do not
1625
+ claim protection from a malicious process running as the same principal or an
1626
+ administrator, nor eliminate same-principal time-of-check/time-of-use races.
1627
+
1628
+ ### Claim reason codes and 8.3 short paths
1629
+
1630
+ A rejected storage claim keeps its user-facing message and appends one fixed
1631
+ code naming the failed check, for example `... policy denied it. Reason: home-owner`.
1632
+ The code never contains a path, SID or exception text. The lines after it name
1633
+ each candidate folder and, for `acl-mask`, the principal and its rights (see
1634
+ [claim candidates](#claim-candidates-and-owners)). Intentional rejections
1635
+ report their own code:
1636
+
1637
+ | Code | Rejected check |
1638
+ |------|----------------|
1639
+ | `policy` | PowerShell is not in FullLanguage mode. |
1640
+ | `path-mismatch` | The candidate base (`%LOCALAPPDATA%` or `%USERPROFILE%`) is not rooted, the target is UNC, or the target's parent is not exactly that base. |
1641
+ | `home-owner` | The candidate base is not owned by the invoking SID, SYSTEM or Administrators. On `%LOCALAPPDATA%` it leads to the `%USERPROFILE%` candidate. |
1642
+ | `ancestor-reparse` | An ancestor is not a directory or is a reparse point. |
1643
+ | `ancestor-owner` | An ancestor owner is not the invoking SID, SYSTEM, Administrators or TrustedInstaller. |
1644
+ | `acl-mask` | An untrusted effective allow ACE exceeds the depth's allowed rights mask. On `%LOCALAPPDATA%` it leads to the `%USERPROFILE%` candidate. |
1645
+ | `collision` | The random destination already exists; it is never reused or changed. |
1646
+ | `protected-dacl` | The readback DACL is not protected from inheritance. |
1647
+ | `private-owner` | The readback owner is not the invoking SID. |
1648
+ | `private-ace` | The readback holds a deny ACE or a SID other than the invoking SID, SYSTEM or Administrators. |
1649
+
1650
+ Any other exception, such as an unreadable ACL, a denied write or a failed
1651
+ cmdlet, reports `unexpected-<step>` for the step that was running, so it can
1652
+ never pose as an intentional rejection:
1653
+
1654
+ | Step | Work in progress |
1655
+ |------|------------------|
1656
+ | `unexpected-policy` | Language mode and identity lookup. |
1657
+ | `unexpected-path-mismatch` | Path normalization. |
1658
+ | `unexpected-home-owner` | Reading the candidate base's owner. |
1659
+ | `unexpected-ancestor-walk` | Reading an ancestor's attributes or ACL. |
1660
+ | `unexpected-create` | Creating the new directory, including a destination that appeared after the collision check. |
1661
+ | `unexpected-private-acl` | Writing or reading back the private DACL. |
1662
+ | `unexpected-marker` | Writing the ownership marker. |
1663
+
1664
+ The Node probe stage follows the same contract. Its message is unchanged and
1665
+ ends with `Reason: <code>`. Walk codes are prefixed with the role of the path
1666
+ component that failed: `target` (`node.exe` itself, depth 0), `parent` (its
1667
+ directory, depth 1) or `ancestor` (depth 2 and above). The role is fixed text,
1668
+ never the path.
1669
+
1670
+ | Code | Rejected check |
1671
+ |------|----------------|
1672
+ | `policy` | PowerShell is not in FullLanguage mode. |
1673
+ | `missing-target` | The `.node-target` record is absent, not a file, or empty. |
1674
+ | `unsafe-target` | The recorded Node target is a directory or a reparse point. |
1675
+ | `unsafe-path` | The recorded Node target is not rooted, or is UNC. |
1676
+ | `<role>-reparse` | A walked component is a reparse point. |
1677
+ | `<role>-owner` | A walked component's owner is not the invoking SID, SYSTEM, Administrators or TrustedInstaller. |
1678
+ | `<role>-acl-mask` | An untrusted effective allow ACE on a walked component exceeds its depth's allowed rights mask. |
1679
+ | `no-start` | The process did not start. |
1680
+ | `deadline` | Node did not exit and close both pipes within 10 seconds. |
1681
+ | `output-limit` | Combined stdout and stderr exceeded 1 MiB. |
1682
+ | `exit-code` | Node exited with a nonzero code. |
1683
+ | `version-format` | `node --version` did not print exactly `vMAJOR.MINOR.PATCH`. |
1684
+ | `engine` | The acquired Node is below 24.3.0 or the repository `engines.node` floor. |
1685
+ | `acquired-version` | A Node acquired by this bootstrap is not exactly v24.21.0. |
1686
+
1687
+ The user's Node (no `.node-stem`) is never refused for `engine` or for the
1688
+ storage codes `unsafe-target`, `unsafe-path` and `<role>-<reparse|owner|acl-mask>`:
1689
+ the stage removes its `.node-target` record, and the entry jumps back to the
1690
+ download, ZIP and probe stages to acquire the pinned Node exactly as when Node
1691
+ is missing. A Node that fails the walk is never started. The acquired Node then
1692
+ has a `.node-stem`, so it can only pass or refuse; every other code still
1693
+ refuses, including `version-format` for an unknown or prerelease version.
1694
+
1695
+ Any other exception reports `unexpected-<step>`: `unexpected-policy`,
1696
+ `unexpected-target` (reading the recorded target), `unexpected-acl-walk`
1697
+ (reading a component's attributes or ACL), `unexpected-start` (for example, an
1698
+ application-control denial while starting the process), `unexpected-drain`
1699
+ (reading the pipes) or `unexpected-version` (reading the repository metadata).
1700
+ A child that cannot be confirmed terminated keeps its separate
1701
+ `direct-child termination could not be confirmed` message.
1702
+
1703
+ The `.node-target` record holds a full path, which may contain non-ASCII text,
1704
+ for example a user profile such as `C:\Users\José` or the Unicode native
1705
+ fixture root. Windows PowerShell 5.1 writes `New-Item -Value` content as
1706
+ BOM-less UTF-8 but reads `Get-Content` without a BOM as the ANSI code page, so
1707
+ such a path came back corrupted and the probe failed with `unexpected-target`.
1708
+ The record is now created no-clobber with `[IO.File]::Open(..., CreateNew)` and
1709
+ explicit BOM-less UTF-8 bytes, and the probe and launch stages read it with
1710
+ `[IO.File]::ReadAllText(path, [Text.Encoding]::UTF8)`. The other bootstrap
1711
+ records (`.bootstrap-owned`, `.node-stem`) hold fixed ASCII text and keep their
1712
+ cmdlets.
1713
+
1714
+ The Node helper (`installer-downloads.mjs --bootstrap-windows`) follows the same
1715
+ contract. Its failure line is unchanged and ends with `Reason: <code> (<step>)`
1716
+ for an intentional rejection, or `Reason: unexpected-<step>` for any other
1717
+ error, such as a file-system, JSON or spawn error. A value without that fixed
1718
+ shape, including a failed module import, prints `Reason: unexpected-helper`.
1719
+
1720
+ | Step | Phase |
1721
+ |------|-------|
1722
+ | `bundle` | Native platform, bundle files and repository Node/pnpm metadata. |
1723
+ | `tools-storage` | ACL walk of the claimed tools directory. |
1724
+ | `pnpm-discovery` | CMD-equivalent search of the current directory, `Path` and `PATHEXT` for `pnpm`. |
1725
+ | `wrapper-storage`, `wrapper` | ACL walk of the found `pnpm.cmd`/`pnpm.exe` and its [exact shim](#windows-command-shims) proof. |
1726
+ | `node-discovery` | The wrapper's sibling `node.exe`, or `node` on `Path`. |
1727
+ | `node-storage`, `entry-storage`, `metadata-storage` | ACL walk of the selected Node, the pnpm entry and its `package.json`. |
1728
+ | `package` | The pnpm `package.json` binary target. |
1729
+ | `cli-proof` | `node --version`, `pnpm --version` and the `add`/`bin` global capability. |
1730
+ | `exe-storage`, `exe-proof` | ACL walk of a native `pnpm.exe` and the `package.json` beside it, then its `--version` and global capability. |
1731
+ | `tools-check`, `download`, `archive`, `publish` | Pinned pnpm acquisition when no `pnpm` was found. |
1732
+ | `launch` | The bundle wizard entry and its exit code. |
1733
+
1734
+ Codes: `native-unavailable`, `bundle-missing`, `prerequisite`, `unsafe-path`,
1735
+ `acl-evidence`, the storage walk codes `policy` and
1736
+ `<target|parent|ancestor>-<reparse|owner|acl-mask>`, `cwd-search`,
1737
+ `path-missing`, `pathext`, `path-entry`, `extensionless`, `wrapper-unknown`,
1738
+ `wrapper-unproven`, `wrapper-node`, `wrapper-target`, `package-target`,
1739
+ `pnpm-engine`, `pnpm-version`, `pnpm-capability`, `process-failed`,
1740
+ `interpreter`, `unsafe-tools`, `pnpm-conflict`, `archive`, `pnpm-pin`,
1741
+ `pnpm-entry`, `acquisition`, `wizard-missing`, `wizard-start` and
1742
+ `wizard-exit`. For example, `parent-owner (tools-check)` means the
1743
+ directory holding the claimed tools directory has an untrusted owner. A walk
1744
+ code on the user's own pnpm (`wrapper-storage`, `node-storage`,
1745
+ `entry-storage`, `metadata-storage`, `exe-storage`) is not a failure: that
1746
+ pnpm is never run and the pinned one is acquired. The helper's storage check
1747
+ prints `unsafe:<code>` for a walk rejection and still accepts only exact `safe`;
1748
+ any other PowerShell exception exits nonzero and reports `unexpected-<step>`.
1749
+
1750
+ `.CPL` deserves a note. Windows PowerShell 5.1 appends `.CPL` to `PATHEXT` in
1751
+ its own environment, so its children see
1752
+ `.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.CPL` even when CMD did
1753
+ not. The bootstrap always starts the helper from Windows PowerShell, and native
1754
+ CI showed the old discovery allowlist rejecting every Windows run with
1755
+ `pathext (pnpm-discovery)`. `.cpl` is now a known extension, but not an
1756
+ accepted candidate. Discovery keeps the CMD order (current directory, then each
1757
+ `Path` entry, then `PATHEXT` order within a directory). A `pnpm.cpl` found
1758
+ before the real `pnpm.cmd` stops with `wrapper-unknown (wrapper)`, and a
1759
+ `node.cpl` found before `node.exe` stops with `wrapper-node (node-discovery)`;
1760
+ neither is skipped or executed. Any other unknown, duplicate or empty
1761
+ `PATHEXT` extension still fails with `pathext`.
1762
+
1763
+ The wizard inherits the same environment. The runner's `lookPath` (Go
1764
+ `exec.LookPath` order) and the host probes accept any `PATHEXT` extension and
1765
+ already refuse a `.cpl` result: npm reports `npm-shadowed`, and Node or Go
1766
+ resolving to a non-`.exe`/`.com` file reports unknown without running it.
1767
+
1768
+ ### Windows command shims
1769
+
1770
+ Node.js refuses to spawn a `.cmd` with `shell:false`, and the installer never
1771
+ starts cmd.exe: a `.cmd` runs a shell, and its paths and arguments would become
1772
+ shell text. Instead, `windowsShim` (`scripts/installer-windows.mjs`) reads a
1773
+ shim and accepts it only when it is, byte for byte apart from CRLF, one its
1774
+ generator writes, with a single quoted target that holds no `"` or `%`. The
1775
+ installer then runs that target itself with `shell:false`
1776
+ (`windowsInvocation` in the runner, `ensureWindowsPnpm` in the bootstrap):
1777
+
1778
+ | Shim | Writer | What runs |
1779
+ | --- | --- | --- |
1780
+ | npm cmd-shim 4.1-8 and 9, JS target | `npm install -g` (pnpm, Pi, npm itself in `%APPDATA%\npm`) | The shim's sibling `node.exe`, else the first `node` on PATH (an `.exe`), with the JS entry. |
1781
+ | npm cmd-shim, native target | `npm install -g pnpm@12` or `@pnpm/exe` | That `.exe` (an extensionless @pnpm/exe hard link runs as its `.exe` twin). |
1782
+ | @zkochan/cmd-shim 9, native or JS target | pnpm 10-12 global bins: `pnpm setup`, `pnpm self-update`, `pnpm add -g` | The `.exe`, or the JS entry with the sibling/PATH Node or the `node.exe` the shim names. pnpm's optional NODE_PATH block only extends module lookup and is not reproduced. |
1783
+ | Node.js's `npm.cmd` (npm 6-9, and 10-11 with `npm-prefix.js`) | Node.js, also behind nvm-windows (`mklink /D`) or fnm (a junction) | Node as above with `node_modules\npm\bin\npm-cli.js`, after npm's own redirect: the fixed prefix query (`npm-prefix.js`, or `npm-cli.js prefix -g`) from the drive root, then the `npm-cli.js` under that prefix when it exists. |
1784
+ | Any `.exe` | Volta (`C:\Program Files\Volta\npm.exe`), mise's default `exe` shims, pnpm's installer | Itself. |
1785
+
1786
+ Anything else (mise's `file` shims, Volta's package shims, Corepack's pnpm, a
1787
+ hand-written wrapper) is never run: an npm is then not usable and the plan
1788
+ persists the installer's npm, a Pi is unknown, and the bootstrap refuses an
1789
+ unknown pnpm wrapper as before, except Corepack's `pnpm.cmd` and mise's file
1790
+ shim, which it treats as no pnpm and acquires its own. A `pnpm.cmd` runs only
1791
+ pnpm's own `node_modules\pnpm\bin\pnpm.cjs` or `pnpm.mjs` entry, so Corepack's
1792
+ `pnpm.js` never runs.
1793
+
1794
+ An extensionless native target (an @pnpm/exe hard link) resolves the way CMD
1795
+ resolves it: through PATHEXT in order, and only when that first match is an
1796
+ `.exe`; a `.com`, `.bat` or `.cmd` found first, or none, is not run.
1797
+
1798
+ Only local drive paths (`C:\...`) count: the command, the Node a shim selects
1799
+ from PATH and npm's global prefix. `lookPath` skips any other PATH entry (UNC,
1800
+ `\\?\`, `\\.\` or a drive-less rooted path) without touching it, since even a
1801
+ lookup can reach a remote share, and `windowsInvocation` rejects such a command
1802
+ or Node and keeps the bundled npm for such a prefix. Sources: the npm/cmd-shim tap snapshots
1803
+ (v4.1.0-v9.0.2), @zkochan/cmd-shim 9.0.8 and npm 6.14.18-11.19.0 `bin/npm.cmd`
1804
+ from the npm registry, volta-cli/volta `wix/main.wxs`, jdx/mise `src/shims.rs`,
1805
+ Schniz/fnm `src/fs.rs` and coreybutler/nvm-windows `src/nvm.go`.
1806
+
1807
+ These writers also put an extensionless Git Bash script beside each `.cmd`.
1808
+ CMD runs the `PATHEXT` match in that directory, so the bootstrap's discovery
1809
+ does too; an extensionless file alone in a directory still stops it with
1810
+ `extensionless`. The native CI lanes compare what CMD runs with what the
1811
+ installer runs for the runner's real `npm.cmd` and an npm cmd-shim `pnpm.cmd`.
1812
+
1813
+ ACLs are read and written with .NET Framework APIs
1814
+ (`[IO.Directory]::GetAccessControl`, `[IO.File]::GetAccessControl`,
1815
+ `[IO.Directory]::SetAccessControl`) instead of `Get-Acl`/`Set-Acl`. Those
1816
+ cmdlets live in Microsoft.PowerShell.Security, which Windows PowerShell 5.1
1817
+ autoloads from `PSModulePath`. Started from a PowerShell 7 terminal (or the
1818
+ GitHub Actions `pwsh` step shell), the inherited `PSModulePath` lists
1819
+ PowerShell 7's Core-only copy first, and 5.1 fails with
1820
+ `CouldNotAutoloadMatchingModule`. The checks themselves are unchanged: the
1821
+ same owner, mask, protected-DACL, readback and reparse predicates in the same
1822
+ order. The bootstrap still uses cmdlets from Microsoft.PowerShell.Management
1823
+ (`Get-Item`, `Get-Content`, `New-Item`, `Remove-Item`, `Test-Path`,
1824
+ `Join-Path`) and Microsoft.PowerShell.Utility (`New-Object`, `ConvertFrom-Json`,
1825
+ `Get-FileHash`, `Add-Type`, `Select-Object`, `Start-Sleep`). The native fixtures
1826
+ observed both modules loading under the same `pwsh` parent, and a module loads
1827
+ as a unit. `Get-Command` is built into the engine.
1828
+
1829
+ The path comparison is insensitive to 8.3 short names by construction: both
1830
+ sides derive from the same `%LOCALAPPDATA%` string and pass through
1831
+ `[IO.Path]::GetFullPath`, so a short `%LOCALAPPDATA%` or `%TEMP%` is never
1832
+ compared with a long canonical form. The host probe adapter's `realpath` is the
1833
+ native call and returns the long form, for example `C:\Users\runneradmin\...`
1834
+ for a `%TEMP%` exposed as `C:\Users\RUNNER~1\...`; tests compare it with
1835
+ `realpathSync.native`, not the JavaScript `realpathSync`, which keeps short
1836
+ names.
1837
+
1838
+ An earlier CI run reported `home-owner` for every native claim. The actual
1839
+ cause was this module-load failure, reported as the step that was running;
1840
+ `unexpected-<step>` now separates the two. The `home-owner` check first
1841
+ required the base to be owned by the invoking SID itself. A real
1842
+ `%LOCALAPPDATA%` created by the User Profile Service is owned by the user, but
1843
+ a beta tester's profile was owned by SYSTEM, and elevated members of
1844
+ Administrators may create new directories owned by BUILTIN\Administrators,
1845
+ depending on the default-owner policy. The check now also accepts SYSTEM and
1846
+ Administrators as the base owner (see
1847
+ [claim candidates](#claim-candidates-and-owners)); any other owner, including
1848
+ TrustedInstaller, still fails closed with `home-owner`. The native fixtures
1849
+ still set the invoking SID as owner of the fixture root, read it back and fail
1850
+ loudly before any production stage runs. The candidate tests also hand one
1851
+ fixture base to Administrators on purpose.
1852
+
1853
+ ### Implemented fixtures versus missing execution evidence
1854
+
1855
+ The native gates now contain runnable assertions, not empty or always-skipped
1856
+ callbacks. They run on Windows without a pending-fixture override:
1857
+
1858
+ - Actual production claim/check predicates: protected-root creation, collision
1859
+ preservation, harmless distant sibling creation, dangerous-right refusal,
1860
+ strict target/immediate-parent checks and junction rejection. Junction creation
1861
+ alone can be capability-skipped after an actual permission/unsupported error.
1862
+ DACL changes are limited to new disposable fixture-owned objects.
1863
+ - Actual fixed PowerShell ZIP namespace/publication stage: local ZIPs with the
1864
+ fixed archive/member identity; valid publication and traversal, aliases,
1865
+ symlink/reparse attributes, ADS, devices, trailing names and parent conflicts.
1866
+ The valid member contains the available fixture Node's unchanged bytes and is
1867
+ never executed. Collision tests preserve unrelated published storage.
1868
+ - Actual pre-Node process primitive: approved Node executes local fixture JS;
1869
+ nonzero exit, both output limits, quiet/stdout/stderr/both-pipe hangs and valid
1870
+ output followed by a hang are rejected. Production drain/deadline/validation/
1871
+ cleanup lines are unchanged. An independent hard guard checks recorded process
1872
+ creation ticks and only kills fresh fixture-owned handles. A guard firing or
1873
+ finding a residual recorded child is a test failure, not successful deadline
1874
+ evidence; fixture cleanup cannot mask a missing production reap.
1875
+ - Complete local sentinel composition exercises CMD continuation/quoting,
1876
+ bundle checks, claim, existing-Node resolution/probe, real helper pnpm handoff
1877
+ and late cleanup with spaces, Unicode and CMD metacharacters. Only the approved
1878
+ available Node's unchanged bytes are cloned into the disposable fixture PATH,
1879
+ excluding operator npm/Corepack/pnpm shims. A local JS pnpm fixture provides
1880
+ read-only identity/help responses. The trusted fixture helper copy has a fixed,
1881
+ fail-only fetch guard: accidental acquisition throws before any network call;
1882
+ it never supplies bytes/digests or adds a production integrity-bypass switch.
1883
+ No network/install occurs.
1884
+
1885
+ The fixtures preserve the exact production command lines and insert only
1886
+ fixture-owned process observations. They compose primitives in a disposable
1887
+ `.cmd`, not a loaded/evaluated PowerShell script file. Native AppLocker and
1888
+ ConstrainedLanguage denials still apply, with no policy relaxation. This is
1889
+ **stage/composition evidence, not uninstrumented whole-entry certification**:
1890
+ transport/integrity gates and the real pinned artifacts require separate proof.
1891
+ The original missing-entry test also exercises the unchanged whole entry.
1892
+
1893
+ All these native fixtures are **implemented but unrun on this Linux host**:
1894
+ Windows PowerShell and a Windows runner are unavailable. Portable descriptor,
1895
+ ACL-predicate model, tar, wrapper and process fixtures are not native Windows
1896
+ proof. Native OS/ACL/CMD/loader acceptance remains T7 evidence work; the actual
1897
+ pinned pnpm TGZ offline acceptance remains an explicitly unfinished evidence
1898
+ item. No live artifact was downloaded/executed, operator-home installation
1899
+ performed, operator/global ACL or policy changed, or Windows OS minimum/support
1900
+ claimed. No same-principal-adversary or forked-descendant guarantee is made.
1901
+ T7 must establish those facts before advertising clean-machine support.
1902
+
1903
+ ## Clean-machine acceptance (T7)
1904
+
1905
+ ### Linux clean container
1906
+
1907
+ A disposable `debian:bookworm-slim` container without Node, npm, pnpm, Pi or
1908
+ Gentle Shell, driven as a non-root user, passed end to end:
1909
+ `sh scripts/bootstrap.sh` exited 0 and the wizard, driven over HTTP exactly as
1910
+ the page does, reached `terminal-action-required` with all 13 runner steps
1911
+ `done` in about 27 seconds. A fresh `bash -ic` resolved `node` v24.21.0, `npm`
1912
+ 11.19.0 and `pnpm` 11.1.1 from `$PNPM_HOME/bin`, and `gentle-shell --version`
1913
+ reported 4.0.0 with Pi 1.0.0 and the isolated home `~/.gentle-shell/agent`.
1914
+ That run predates the removal of bootstrap tools after success.
1915
+
1916
+ The first run of `scripts/test-installer-acceptance.sh --worktree` (with the
1917
+ tools removal, not yet committed) did **not** pass: the bootstrap acquired its
1918
+ tools and printed the session URL after 113 seconds, the plan had no blockers,
1919
+ and 11 runner steps finished `done` before `shell-setup` (`gentle-shell setup`)
1920
+ failed; the outcome was `failed`, the bootstrap exited 1 and removed its tools
1921
+ through the failure path, after 146 seconds in total. A separate diagnostic
1922
+ container proved the cause: `gentle-shell setup` failed with `Error: execute
1923
+ install pipeline: download engram binary: fetch latest engram version: GitHub
1924
+ API returned HTTP 403` because the anonymous GitHub API quota of this network
1925
+ was exhausted. After the quota reset, setup in the same container exited 0
1926
+ without installing any extra package. It was not caused by the tools removal,
1927
+ which only runs after the wizard exits. See
1928
+ [GitHub API limit during setup](#github-api-limit-during-setup).
1929
+
1930
+ The second run (with the quota precheck: 43 anonymous requests left) got past
1931
+ that: `shell-setup` finished `done`, then the last step, `persist-path`
1932
+ (`pnpm setup`), failed. The installation part took about 34 seconds, so it was
1933
+ not its 30-second deadline alone. The outcome was `failed`, the bootstrap exited
1934
+ 1 and removed its tools through the failure path; 147 seconds in total. Cause:
1935
+ the script started the bootstrap with `docker exec ... sh -c`, which exports no
1936
+ `SHELL`, while the passing lab run had `SHELL=/bin/bash`; `pnpm setup` then
1937
+ cannot pick a profile (see `persist-path` above). A real terminal exports
1938
+ `SHELL`, so the script now passes `SHELL=/bin/bash` to the bootstrap, and a
1939
+ failed `persist-path` now reports pnpm's error line and SHELL guidance.
1940
+
1941
+ The third run, with `SHELL=/bin/bash`, **passed**: 23 anonymous GitHub
1942
+ requests left before it, session URL after 113 seconds, all 13 runner steps
1943
+ `done` including `shell-setup` and `persist-path`, outcome
1944
+ `terminal-action-required`, bootstrap exit 0; 142 seconds for bootstrap and
1945
+ wizard, 147 in total. A fresh `bash -ic` resolved `node` v24.21.0, `npm`
1946
+ 11.19.0, `pnpm` 11.1.1 and `gentle-shell` from `~/.local/share/pnpm/bin`;
1947
+ `gentle-shell --version` reported 4.0.0, Pi 1.0.0 and the isolated home
1948
+ `~/.gentle-shell/agent`; no `~/.gentle-shell-bootstrap-tools.*` directory
1949
+ remained, so the success-path tools removal is proven in a clean container.
1950
+ `bash -lc` did not resolve `gentle-shell`, as documented below.
1951
+
1952
+ To reproduce it, run `sh scripts/test-installer-acceptance.sh` from the
1953
+ repository. It refuses to start without a working Docker daemon and never runs
1954
+ the installer on the host or touches the host HOME. It starts a container
1955
+ (`--pull missing`), installs only `ca-certificates` and `curl`, creates the
1956
+ user `tester`, then queries `https://api.github.com/rate_limit` from inside the
1957
+ container (this endpoint does not consume quota) and stops before the
1958
+ bootstrap, printing the reset time, when fewer than 5 anonymous core requests
1959
+ remain. It then copies the bundle to `/opt/bundle` (`git archive HEAD`, or the
1960
+ tracked working-tree files including uncommitted changes with `--worktree`) and
1961
+ runs the bootstrap. A headless driver inside the container redeems
1962
+ `/session?code=` with a cookie jar, sends `X-Gentle-Install: 1` on every
1963
+ `/api/*` request and the same Origin plus `Content-Type: application/json` on
1964
+ every POST, then requests the plan, consents, polls progress and shuts the host
1965
+ down. It then checks, in a fresh `bash -ic`, that `node`, `npm`, `pnpm` and
1966
+ `gentle-shell --version` resolve and that no `~/.gentle-shell-bootstrap-tools.*`
1967
+ directory remains, and reports whether `bash -lc` resolves `gentle-shell`. The
1968
+ container is removed on every exit unless `GENTLE_ACCEPTANCE_KEEP=1` keeps a
1969
+ failed one for inspection; the pulled image stays cached. Network
1970
+ access happens only inside the container. The installation is bounded by
1971
+ `GENTLE_ACCEPTANCE_INSTALL_SECONDS` (default 1500). CI does not run it.
1972
+
1973
+ ### GitHub API limit during setup
1974
+
1975
+ `gentle-shell setup` runs upstream Gentle AI's install pipeline, which looks up
1976
+ the latest Engram version through the GitHub REST API without authentication:
1977
+ 2 anonymous requests per setup. GitHub allows 60 anonymous requests per hour
1978
+ per public IP address, so machines sharing one address (offices, NAT, VPNs, CI
1979
+ runners, repeated test runs) can exhaust it; setup then fails with
1980
+ `GitHub API returned HTTP 403` and the wizard shows the rate-limit guidance and
1981
+ that error line. Waiting for the hourly reset is enough; nothing has to be
1982
+ uninstalled first: rerunning the installer finds the installed stack and runs
1983
+ only its [setup recovery](#setup-recovery).
1984
+
1985
+ A missing `git` only produces a warning during setup; it was not the cause.
1986
+
1987
+ Upstream advisory for Gentle AI (not changed here): pin the Engram version or
1988
+ fall back to it when the lookup fails, accept an optional `GITHUB_TOKEN` for
1989
+ the lookup, and replace the misleading "installed via go install" message that
1990
+ accompanies this failure.
1991
+
1992
+ ### Shell profiles after `pnpm setup`
1993
+
1994
+ `pnpm setup` writes `PNPM_HOME` and its PATH entry into the interactive shell's
1995
+ profile only: `~/.bashrc` for bash on Debian. Debian's `~/.bashrc` returns
1996
+ early for non-interactive shells, and login shells read `~/.profile`, so these
1997
+ do not find `gentle-shell`, `node`, `npm` or `pnpm`:
1998
+
1999
+ - non-interactive login shells such as `bash -lc '...'`, and tools or IDE tasks
2000
+ that start one;
2001
+ - `ssh host command`: bash reads `~/.bashrc` but stops at its non-interactive
2002
+ guard, so the lines must sit above that guard (or the command must use an
2003
+ absolute path);
2004
+ - cron jobs, which read neither file: set PATH in the crontab or use absolute
2005
+ paths.
2006
+
2007
+ For login shells, add the same lines to `~/.profile` manually (the wizard does
2008
+ not edit it), using the `PNPM_HOME` that `pnpm setup` wrote, for example:
2009
+
2010
+ ```sh
2011
+ export PNPM_HOME="$HOME/.local/share/pnpm"
2012
+ case ":$PATH:" in *":$PNPM_HOME/bin:"*) ;; *) export PATH="$PNPM_HOME/bin:$PATH" ;; esac
2013
+ ```
2014
+
2015
+ ### Upstream `~/.pi/gentle-ai`
2016
+
2017
+ Gentle AI's setup creates an empty `~/.pi/gentle-ai` directory outside the
2018
+ isolated Gentle Shell home `~/.gentle-shell/agent`. This is upstream behavior;
2019
+ the installer neither relies on nor removes it.
2020
+
2021
+ ### CI matrix
2022
+
2023
+ The `installer` job in `.github/workflows/ci.yml` runs the installer suites with
2024
+ Node 24 on `ubuntu-latest`, `macos-latest` and `windows-latest`. The native
2025
+ Windows tests in `tests/installer-windows-bootstrap.test.ts` are gated on
2026
+ `process.platform === "win32"`, so they run on the Windows runner and skip
2027
+ elsewhere; the POSIX shell fixtures in `tests/installer-posix-bootstrap.test.ts`
2028
+ skip on Windows. CI only runs after a push, which remains a user decision.
2029
+
2030
+ ### Remaining T7 checks
2031
+
2032
+ - real browsers and screen readers (only headless Chromium was used);
2033
+ - native macOS and Windows clean machines, including the Windows bootstrap
2034
+ end to end and its tools removal;
2035
+ - `pnpm setup` and fresh-terminal resolution in zsh and fish;
2036
+ - SIGTERM during a running installation;
2037
+ - reinstalling over an existing stack;
2038
+ - download-time integrity of the Node runtime and the npm and pnpm packages
2039
+ fetched during persistence;
2040
+ - the earlier T4/T5/T6 items listed above that the Linux run does not cover.