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,648 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+ import { closeSync, lstatSync, openSync, readFileSync, readSync, realpathSync, mkdirSync, mkdtempSync, writeFileSync, renameSync, rmSync } from "node:fs";
3
+ import { basename, dirname, join, resolve, delimiter } from "node:path";
4
+ import { spawnSync, spawn } from "node:child_process";
5
+ import { pathToFileURL } from "node:url";
6
+ import { crc32, gunzipSync, inflateRawSync } from "node:zlib";
7
+
8
+ // Acquisition pins, not compatibility minima. Sources verified before pinning:
9
+ // https://nodejs.org/dist/latest-v24.x/SHASUMS256.txt
10
+ // https://registry.npmjs.org/pnpm/11.1.1 (dist.integrity and engines.node)
11
+ const nodeHashes = Object.freeze({
12
+ "darwin-arm64": "bed7eea5325e1108f32ce5228ddd6a5f0f08a499ee42aa7442aea583702f6057",
13
+ "darwin-x64": "1462cb3b3046b815cf8ea436d3da450ec1a9f11dac7e5a46b0ada5305d7e8097",
14
+ "linux-arm64": "724282c3b43aec998aa9527380465b45d229e021b58035f5f4f63095eabfe5d5",
15
+ "linux-x64": "6e1db87ef58b8819e5d5402eff1536491b18edd8eb7bee5ef7897876e88dc5ff",
16
+ });
17
+ const pnpm = Object.freeze({
18
+ name: "pnpm", version: "11.1.1", engine: ">=22.13",
19
+ url: "https://registry.npmjs.org/pnpm/-/pnpm-11.1.1.tgz",
20
+ integrity: "sha512-0f319zxhe2T6GlaoHDyN/g6WbjOmAQqiVrUXrne+Idk+Ba/8DeGoOw5PKdVp9otEaujwaM1yR8C7PfD7TXvfmg==",
21
+ maxBytes: 32 * 1024 * 1024,
22
+ });
23
+ // Go toolchain used only to build Gentle AI: https://go.dev/dl/?mode=json&include=all
24
+ // lists each archive's sha256 and size; every archive was downloaded and re-hashed
25
+ // before pinning. go.dev/dl/<file> redirects to dl.google.com/go/<file>, and
26
+ // download() refuses redirects, so the pins name the final URL.
27
+ const go = Object.freeze({
28
+ version: "1.25.14",
29
+ maxExpandedBytes: 512 * 1024 * 1024,
30
+ archives: Object.freeze({
31
+ "darwin-arm64": ["darwin-arm64.tar.gz", "5b26c0b6f308240fca2614fb02f622cfcc8c0cc3b69c78bba4845489a4590259", 58123934],
32
+ "darwin-x64": ["darwin-amd64.tar.gz", "b09087a67d5792a8b0fcbf74212d62560c94ac8a8fd750ff920d8cb5f1e20118", 60622678],
33
+ "linux-x64": ["linux-amd64.tar.gz", "a21ae5633a269bcd7e90cf767e48225633795e99d831742cbf3397064fee7712", 59909419],
34
+ "linux-arm64": ["linux-arm64.tar.gz", "9bf234ea70ffec9347fdf6b22ce4add51717d3386a38a441e8c8743fceb5eaee", 57360344],
35
+ "win32-x64": ["windows-amd64.zip", "119044a92b3987c341cd6aebb256676dd4780d292f7b4e72a3e9976677841697", 67591780],
36
+ "win32-arm64": ["windows-arm64.zip", "96fb31ae26b288b5311bd31d8252d4a62c8a661e4dbb64d504cc646e4d10a57f", 64735369],
37
+ }),
38
+ });
39
+ /** The Go version acquireGo publishes. */
40
+ export const goPinVersion = go.version;
41
+ // Engram for the bundled install (GENTLE_AI_ENGRAM_BIN and the agent's memory):
42
+ // the Gentleman-Programming/engram release gentle-ai 4.x installs (it takes the
43
+ // latest core release). Each sha256 and size comes from the release's
44
+ // checksums.txt and GitHub's asset digests; every archive was downloaded and
45
+ // re-hashed before pinning. GitHub serves the assets from its asset hosts.
46
+ const engram = Object.freeze({
47
+ version: "3.4.0",
48
+ redirectHosts: Object.freeze(["objects.githubusercontent.com", "release-assets.githubusercontent.com"]),
49
+ archives: Object.freeze({
50
+ "darwin-arm64": ["darwin_arm64.tar.gz", "d0aa59c9a83d17ae3fc7f5dd31fdab6a26fd8c00fc5e692195753cd0719294f7", 7470202],
51
+ "darwin-x64": ["darwin_amd64.tar.gz", "3c5bdc73d226af5629cf4f4a781e223142e37c1ebb443c4b596f2e284352e381", 7961674],
52
+ "linux-x64": ["linux_amd64.tar.gz", "715fda625556e46d65ddb4526bb7f88a3fee5339ca3c44ce44cc652f17fa57f1", 7821191],
53
+ "linux-arm64": ["linux_arm64.tar.gz", "3309e9c0bbb539d183fd5aa9f9a13cd7aa0e81ec2512b5bedefb8b2bdd35b3e5", 7176726],
54
+ "win32-x64": ["windows_amd64.zip", "3065736ec7ff8137eb3fc607275f542ea13bb743a5157318c8a8770f11ce3289", 8021225],
55
+ "win32-arm64": ["windows_arm64.zip", "7463872a85a5e5434cf958bdffc71dda1926bced30d3b6b38c80fec9bb98fbba", 7231641],
56
+ }),
57
+ });
58
+ /** The Engram version the bundled install publishes under runtime/. */
59
+ export const engramPinVersion = engram.version;
60
+
61
+ /** Fixed allowlist; callers cannot supply download URLs, checksums or commands. */
62
+ export function artifactFor(name, platform, arch) {
63
+ if (name === "pnpm") return pnpm;
64
+ if (name === "node" && platform === "win32" && ["x64", "arm64"].includes(arch)) {
65
+ const pin = JSON.parse(readFileSync(new URL("./installer-windows-artifacts.json", import.meta.url), "utf8")).node;
66
+ const target = pin[arch];
67
+ if (pin.version !== "24.21.0" || target.url !== `https://nodejs.org/dist/v24.21.0/node-v24.21.0-win-${arch}.zip` || !/^[a-f0-9]{64}$/.test(target.sha256) || pin.maxBytes !== 104857600) {
68
+ throw new Error("Windows artifact metadata rejected");
69
+ }
70
+ return Object.freeze({ name, version: pin.version, platform, arch, url: target.url, integrity: `sha256-${target.sha256}`, maxBytes: pin.maxBytes });
71
+ }
72
+ if (name === "engram") {
73
+ const pin = Object.hasOwn(engram.archives, `${platform}-${arch}`) ? engram.archives[`${platform}-${arch}`] : null;
74
+ if (!pin) throw new Error("Unsupported acquisition target");
75
+ const [suffix, sha256, size] = pin;
76
+ return Object.freeze({ name, version: engram.version, platform, arch,
77
+ url: `https://github.com/Gentleman-Programming/engram/releases/download/v${engram.version}/engram_${engram.version}_${suffix}`,
78
+ integrity: `sha256-${sha256}`, size, maxBytes: size, redirectHosts: engram.redirectHosts });
79
+ }
80
+ if (name === "go") {
81
+ const pin = Object.hasOwn(go.archives, `${platform}-${arch}`) ? go.archives[`${platform}-${arch}`] : null;
82
+ if (!pin) throw new Error("Unsupported acquisition target");
83
+ const [suffix, sha256, size] = pin;
84
+ return Object.freeze({ name, version: go.version, platform, arch, url: `https://dl.google.com/go/go${go.version}.${suffix}`,
85
+ integrity: `sha256-${sha256}`, size, maxBytes: size });
86
+ }
87
+ const hash = nodeHashes[`${platform}-${arch}`];
88
+ if (name !== "node" || !hash) throw new Error("Unsupported acquisition target");
89
+ return Object.freeze({ name, version: "24.21.0", platform, arch,
90
+ url: `https://nodejs.org/dist/v24.21.0/node-v24.21.0-${platform}-${arch}.tar.gz`,
91
+ integrity: `sha256-${hash}`, maxBytes: 100 * 1024 * 1024 });
92
+ }
93
+
94
+ /** The bytes at descriptor.url, at most descriptor.maxBytes. Redirects are
95
+ * refused, except up to 5 to an https host in descriptor.redirectHosts (GitHub
96
+ * serves release assets from another host). A non-2xx answer throws with its
97
+ * HTTP `status`.
98
+ */
99
+ export async function download(descriptor) {
100
+ // identity: fetch would otherwise accept gzip and decode it, so the advertised
101
+ // length (dl.google.com gzips archives on request) would not match the bytes.
102
+ const hosts = Array.isArray(descriptor.redirectHosts) ? descriptor.redirectHosts : null;
103
+ let url = descriptor.url;
104
+ let response;
105
+ for (let hops = 0; ; hops += 1) {
106
+ response = await fetch(url, { redirect: hosts ? "manual" : "error", headers: { "accept-encoding": "identity" }, signal: AbortSignal.timeout(60000) });
107
+ if (!hosts || ![301, 302, 303, 307, 308].includes(response.status)) break;
108
+ await response.body?.cancel();
109
+ const next = new URL(response.headers.get("location") ?? "", url);
110
+ if (hops >= 5 || next.protocol !== "https:" || !hosts.includes(next.hostname)) throw new Error("Download redirect rejected");
111
+ url = next.href;
112
+ }
113
+ if (!response.ok || !response.body) {
114
+ await response.body?.cancel();
115
+ throw Object.assign(new Error(`Download responded ${response.status}`), { status: response.status });
116
+ }
117
+ const encoding = response.headers.get("content-encoding");
118
+ if (encoding && encoding.toLowerCase() !== "identity") {
119
+ await response.body.cancel();
120
+ throw new Error(`Download was encoded (${encoding}) although identity was requested`);
121
+ }
122
+ const advertised = response.headers.get("content-length");
123
+ if (advertised && (!/^\d+$/.test(advertised) || Number(advertised) > descriptor.maxBytes)) {
124
+ await response.body.cancel();
125
+ throw new Error("Download size rejected");
126
+ }
127
+ const reader = response.body.getReader();
128
+ const chunks = [];
129
+ let size = 0;
130
+ try {
131
+ for (;;) {
132
+ const { value, done } = await reader.read();
133
+ if (done) break;
134
+ size += value.byteLength;
135
+ if (size > descriptor.maxBytes) throw new Error("Download size rejected");
136
+ chunks.push(Buffer.from(value));
137
+ }
138
+ if (advertised && Number(advertised) !== size) throw new Error("Download truncated");
139
+ return Buffer.concat(chunks, size);
140
+ } finally {
141
+ await reader.cancel().catch(() => {});
142
+ }
143
+ }
144
+
145
+ /** Pure descriptor selection + injected byte transport/digest for deterministic tests.
146
+ * Production always hashes actual bytes; test adapters (download, digest and the
147
+ * artifact descriptor) are trusted local code only. A descriptor with a size
148
+ * accepts exactly that many bytes.
149
+ * No extraction or execution is allowed before this function succeeds.
150
+ */
151
+ export async function verifiedDownload(name, adapters = {}, platform, arch) {
152
+ const descriptor = (adapters.artifact ?? artifactFor)(name, platform, arch);
153
+ let bytes;
154
+ try { bytes = await (adapters.download ?? download)(descriptor); }
155
+ catch (cause) { throw new Error("Download failed", { cause }); }
156
+ if (!Buffer.isBuffer(bytes) || bytes.length === 0 || bytes.length > descriptor.maxBytes) throw new Error("Download size rejected");
157
+ if (descriptor.size !== undefined && bytes.length !== descriptor.size) throw new Error("Download size rejected");
158
+ const [algorithm, encoded] = descriptor.integrity.split("-");
159
+ const expected = Buffer.from(encoded, algorithm === "sha256" ? "hex" : "base64");
160
+ const actual = (adapters.digest ?? ((data, hash) => createHash(hash).update(data).digest()))(bytes, algorithm);
161
+ if (!Buffer.isBuffer(actual) || actual.length !== expected.length || !timingSafeEqual(actual, expected)) {
162
+ throw new Error("Download integrity mismatch");
163
+ }
164
+ return bytes;
165
+ }
166
+
167
+ function parts(version) {
168
+ if (typeof version !== "string" || !/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(version)) return null;
169
+ const values = version.replace(/^v/, "").split(".").map(Number);
170
+ return values.every(Number.isSafeInteger) ? values : null;
171
+ }
172
+ export function compatibleEngine(range, version) {
173
+ if (typeof range !== "string") return false;
174
+ // Preserve raw pin metadata; normalize only simple >=major.minor[.patch] bounds.
175
+ const bound = /^>=(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?$/.exec(range);
176
+ if (!bound) return false;
177
+ const required = parts(`${bound[1]}.${bound[2]}.${bound[3] ?? "0"}`);
178
+ const actual = parts(version);
179
+ if (!required || !actual) return false;
180
+ for (let index = 0; index < 3; index += 1) {
181
+ if (actual[index] !== required[index]) return actual[index] > required[index];
182
+ }
183
+ return true;
184
+ }
185
+ function stat(path) {
186
+ try { return lstatSync(path); }
187
+ catch (error) { if (error.code === "ENOENT") return null; throw error; }
188
+ }
189
+ function regular(path) {
190
+ const info = stat(path);
191
+ return info?.isFile() && !info.isSymbolicLink();
192
+ }
193
+ function processCheck(command, args, env) {
194
+ // TERM can be ignored, leaving spawnSync blocked beyond its timeout. Kill only
195
+ // the spawned prerequisite child; this is not process-tree cancellation.
196
+ // A neutral cwd: inside a project, pnpm 11 reports the version that
197
+ // project's packageManager pins instead of its own.
198
+ const result = spawnSync(command, args, { cwd: "/", env, encoding: "utf8", timeout: 15000, killSignal: "SIGKILL", maxBuffer: 1024 * 1024, windowsHide: true });
199
+ if (result.error || result.status !== 0) throw new Error("Prerequisite process check failed");
200
+ return result.stdout.trim();
201
+ }
202
+ function findExecutable(name, env) {
203
+ for (const directory of (env.PATH ?? "").split(delimiter)) {
204
+ // Empty/relative PATH entries cannot prove executable ownership.
205
+ if (!directory.startsWith("/")) throw new Error("Unknown prerequisite PATH compatibility");
206
+ const path = join(directory, name);
207
+ if (stat(path)) return path;
208
+ }
209
+ return null;
210
+ }
211
+ function head(path, size) {
212
+ const buffer = Buffer.alloc(size);
213
+ const fd = openSync(path, "r");
214
+ try {
215
+ return buffer.subarray(0, readSync(fd, buffer, 0, size, 0));
216
+ } finally {
217
+ closeSync(fd);
218
+ }
219
+ }
220
+ // pnpm installs itself in $PNPM_HOME behind a regular cmd-shim file, not a
221
+ // symlink. pnpm 11 names its target in a `# cmd-shim-target=` comment; older
222
+ // shims only run the single `"$basedir/<target>" "$@"` next to the shim. The
223
+ // target is a starting point for the evidence search, not proof: the command
224
+ // itself still has to print the version that evidence names. A shim is a
225
+ // script read whole, up to 64 KiB; a larger one fails closed. Anything else,
226
+ // such as a native pnpm, is not a shim.
227
+ const SHIM_LIMIT = 64 * 1024;
228
+ function shimTarget(command) {
229
+ if (!regular(command)) return null;
230
+ const bytes = head(command, SHIM_LIMIT + 1);
231
+ if (bytes.subarray(0, 2).toString("latin1") !== "#!") return null;
232
+ if (bytes.length > SHIM_LIMIT) throw new Error("Existing pnpm compatibility is unknown: shim larger than 64 KiB");
233
+ const text = bytes.toString("utf8");
234
+ const match = /^# cmd-shim-target=(\/[^\r\n]+)$/m.exec(text);
235
+ if (match) return stat(match[1]) ? match[1] : null;
236
+ const targets = new Set([...text.matchAll(/"\$basedir\/([^"$`\\]+)"[ \t]+"\$@"/g)]
237
+ .map(([, target]) => join(realpathSync(dirname(command)), target)));
238
+ const [target] = targets;
239
+ return targets.size === 1 && stat(target) ? target : null;
240
+ }
241
+ function packageFor(target) {
242
+ let directory = dirname(target);
243
+ for (let depth = 0; depth < 4; depth += 1) {
244
+ const file = join(directory, "package.json");
245
+ if (regular(file)) {
246
+ const metadata = JSON.parse(readFileSync(file, "utf8"));
247
+ if (metadata.name === "pnpm") return metadata;
248
+ }
249
+ const parent = dirname(directory);
250
+ if (parent === directory) break;
251
+ directory = parent;
252
+ }
253
+ throw new Error("Existing pnpm compatibility is unknown: package engine evidence missing");
254
+ }
255
+ // mise, asdf and pnpm's own installer (directly or behind its shim, as
256
+ // @pnpm/exe) ship pnpm as a standalone native executable that embeds its own
257
+ // Node runtime, so the user's Node engine does not apply to it. Only a Mach-O
258
+ // or ELF header qualifies; scripts and shims without a package still need
259
+ // package engine evidence.
260
+ const nativeHeaders = ["cffaedfe", "cefaedfe", "feedfacf", "feedface", "cafebabe", "bebafeca", "7f454c46"];
261
+ function standalonePnpm(target) {
262
+ return regular(target) && nativeHeaders.includes(head(target, 4).toString("hex"));
263
+ }
264
+ // The pnpm package a native executable ships in (@pnpm/exe, @pnpm/<platform>,
265
+ // pnpm 12), when there is one: its version is what the executable must print.
266
+ function standaloneVersion(target) {
267
+ const file = join(dirname(target), "package.json");
268
+ if (!regular(file)) return null;
269
+ const metadata = JSON.parse(readFileSync(file, "utf8"));
270
+ return metadata.name === "pnpm" || /^@pnpm\//.test(metadata.name) ? metadata.version : null;
271
+ }
272
+ function provePnpm(command, prefix, metadata, nodeVersion, env, processAdapter, known = null) {
273
+ if (metadata && (!parts(metadata.version) || !compatibleEngine(metadata.engines?.node, nodeVersion))) {
274
+ throw new Error("pnpm compatibility is unknown or incompatible; refusing replacement");
275
+ }
276
+ const version = known ?? processAdapter(command, [...prefix, "--version"], env);
277
+ if (metadata ? version !== metadata.version : !parts(version)) throw new Error("pnpm version rejected");
278
+ // Read-only CLI capability checks; do not execute add/bin or write global config.
279
+ for (const capability of ["add", "bin"]) {
280
+ if (!/(?:^|[\s,])--global(?:[\s,=]|$)/.test(processAdapter(command, [...prefix, "help", capability], env))) {
281
+ throw new Error("pnpm global-install capability evidence missing");
282
+ }
283
+ }
284
+ }
285
+ function privateTools(home) {
286
+ if (!home || !home.startsWith("/") || realpathSync(home) !== resolve(home)) throw new Error("Unsafe tooling HOME");
287
+ const info = lstatSync(home);
288
+ if (!info.isDirectory() || info.uid !== process.getuid() || (info.mode & 0o022)) throw new Error("Unsafe tooling HOME ownership");
289
+ const tools = mkdtempSync(join(home, ".gentle-shell-bootstrap-tools."));
290
+ try {
291
+ writeFileSync(join(tools, ".bootstrap-owned"), "gentle-pi prerequisite tooling only\n", { mode: 0o600, flag: "wx" });
292
+ return tools;
293
+ } catch {
294
+ rmSync(tools, { recursive: true, force: true });
295
+ throw new Error("Unsafe tooling ownership marker could not be created");
296
+ }
297
+ }
298
+
299
+ /** Remove only the exact private `$HOME/.gentle-shell-bootstrap-tools.*` directory
300
+ * carrying this module's marker. rmSync never follows links inside it.
301
+ * Returns false instead of throwing so success never turns into failure.
302
+ */
303
+ export function removeOwnedTools(tools, home) {
304
+ try {
305
+ if (typeof tools !== "string" || typeof home !== "string" || !home.startsWith("/") || resolve(tools) !== tools) return false;
306
+ if (dirname(tools) !== resolve(home) || !basename(tools).startsWith(".gentle-shell-bootstrap-tools.")) return false;
307
+ const info = lstatSync(tools);
308
+ if (!info.isDirectory() || info.isSymbolicLink() || info.uid !== process.getuid()) return false;
309
+ const marker = join(tools, ".bootstrap-owned");
310
+ if (!regular(marker) || readFileSync(marker, "utf8") !== "gentle-pi prerequisite tooling only\n") return false;
311
+ rmSync(tools, { recursive: true });
312
+ return true;
313
+ } catch {
314
+ return false;
315
+ }
316
+ }
317
+
318
+ /** The pnpm the bootstraps run: the pinned major at the pinned version or newer.
319
+ * A user's pnpm of a newer major is left as it is and used by the plan (installer-probes).
320
+ */
321
+ export function pinnedPnpmCompatible(version) {
322
+ const actual = parts(version);
323
+ return actual !== null && actual[0] === parts(pnpm.version)[0] && compatibleEngine(`>=${pnpm.version}`, version);
324
+ }
325
+
326
+ /** Reuse only proven engines/capabilities; missing pnpm gets verified registry bytes.
327
+ * An existing pnpm with stable version evidence of another major, older than the
328
+ * pin, or whose simple engine bound rejects this Node is left as it is, and the
329
+ * verified pnpm is acquired exactly as when pnpm is missing. Unknown evidence
330
+ * still refuses. Local test adapters cover transport/hash/process. There is no
331
+ * remote command API. `tools` is a private directory owned by the invoking
332
+ * bootstrap/controller, or a function claiming one only when acquisition starts.
333
+ */
334
+ export async function ensurePnpm({ tools, env, nodeVersion, adapters = {} }) {
335
+ const processAdapter = adapters.process ?? processCheck;
336
+ const existing = findExecutable("pnpm", env);
337
+ if (existing) {
338
+ const target = realpathSync(shimTarget(existing) ?? existing);
339
+ const standalone = standalonePnpm(target);
340
+ const metadata = standalone ? null : packageFor(target);
341
+ const bound = /^>=(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?$/.test(metadata?.engines?.node ?? "");
342
+ // A standalone pnpm reports its version only by running; that answer is reused.
343
+ const version = metadata ? null : processAdapter(existing, ["--version"], env);
344
+ const shipped = standalone ? standaloneVersion(target) : null;
345
+ if (shipped !== null && version !== shipped) throw new Error("pnpm version rejected");
346
+ const incompatible = metadata
347
+ ? parts(metadata.version) !== null && bound && (!pinnedPnpmCompatible(metadata.version) || !compatibleEngine(metadata.engines.node, nodeVersion))
348
+ : parts(version) !== null && !pinnedPnpmCompatible(version);
349
+ if (!incompatible) {
350
+ provePnpm(existing, [], metadata, nodeVersion, env, processAdapter, version);
351
+ return { env, acquired: false };
352
+ }
353
+ }
354
+ if (typeof tools === "function") tools = tools();
355
+ const info = stat(tools);
356
+ if (!info?.isDirectory() || info.isSymbolicLink() || realpathSync(tools) !== resolve(tools) || info.uid !== process.getuid() || (info.mode & 0o077)) {
357
+ throw new Error("Unsafe private tooling destination");
358
+ }
359
+ const destination = join(tools, "pnpm");
360
+ if (stat(destination)) throw new Error("Conflicting pnpm destination");
361
+ const tar = findExecutable("tar", env);
362
+ if (!tar) throw new Error("Required utility missing: tar");
363
+ const stage = mkdtempSync(join(tools, "pnpm-stage-"));
364
+ let published = false;
365
+ try {
366
+ const archive = join(stage, "pnpm.tgz");
367
+ writeFileSync(archive, await verifiedDownload("pnpm", adapters), { flag: "wx", mode: 0o600 });
368
+ const names = processAdapter(tar, ["-tzf", archive], env).split("\n");
369
+ if (!names.length || names.some((name) => !name.startsWith("package/") || name.split("/").includes("..") || name.includes("\\"))) {
370
+ throw new Error("Unsafe pnpm archive paths");
371
+ }
372
+ const types = processAdapter(tar, ["-tvzf", archive], env).split("\n");
373
+ if (types.some((line) => !/^[d-]/.test(line))) throw new Error("Unsafe pnpm archive links");
374
+ processAdapter(tar, ["-xzf", archive, "-C", stage], env);
375
+ const pkg = join(stage, "package");
376
+ const entry = join(pkg, "bin/pnpm.mjs");
377
+ if (!regular(entry) || !regular(join(pkg, "package.json"))) throw new Error("pnpm archive entry missing");
378
+ const metadata = JSON.parse(readFileSync(join(pkg, "package.json"), "utf8"));
379
+ if (metadata.name !== "pnpm" || metadata.version !== pnpm.version || metadata.engines?.node !== pnpm.engine) throw new Error("pnpm pin metadata rejected");
380
+ provePnpm(process.execPath, [entry], metadata, nodeVersion, env, processAdapter);
381
+ // mkdir is the atomic no-clobber claim (rename alone can replace directories).
382
+ mkdirSync(destination, { mode: 0o700 });
383
+ published = true;
384
+ renameSync(pkg, join(destination, "package"));
385
+ mkdirSync(join(destination, "bin"), { mode: 0o700 });
386
+ const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
387
+ writeFileSync(join(destination, "bin/pnpm"), `#!/bin/sh\nexec ${quote(process.execPath)} ${quote(join(destination, "package/bin/pnpm.mjs"))} "$@"\n`, { flag: "wx", mode: 0o700 });
388
+ return { acquired: true, env: { ...env, PATH: `${join(destination, "bin")}${delimiter}${env.PATH}` } };
389
+ } catch {
390
+ if (published) rmSync(destination, { recursive: true, force: true });
391
+ throw new Error("pnpm verified acquisition failed; no installation completed");
392
+ } finally {
393
+ rmSync(stage, { recursive: true, force: true });
394
+ }
395
+ }
396
+
397
+ /** A Go archive member: under `go/`, with no empty, `.` or `..` segment and no
398
+ * backslash, colon or control character. Returns it without a trailing `/`.
399
+ */
400
+ function goMember(name, directory) {
401
+ const parts = (directory ? name.replace(/\/$/, "") : name).split("/");
402
+ if (name.length > 1024 || parts[0] !== "go" || /[\\:\u0000-\u001f\u007f]/.test(name) ||
403
+ parts.some((part) => part === "" || part === "." || part === "..")) throw new Error("Unsafe Go archive path");
404
+ return parts.join("/");
405
+ }
406
+
407
+ /** A PAX extended header that only renames the next member (`path`, as go.dev
408
+ * uses for non-ASCII names). Any other key could change its meaning: rejected.
409
+ */
410
+ function paxPath(data) {
411
+ let path = null;
412
+ for (let rest = data; rest.length > 0;) {
413
+ const space = rest.indexOf(0x20);
414
+ const digits = rest.subarray(0, Math.max(space, 0)).toString("latin1");
415
+ const length = Number(digits);
416
+ if (!/^[1-9]\d{0,5}$/.test(digits) || length > rest.length || rest[length - 1] !== 0x0a) throw new Error("Unsafe Go archive header");
417
+ const record = rest.subarray(space + 1, length - 1).toString("utf8");
418
+ if (!record.startsWith("path=") || path !== null) throw new Error("Unsafe Go archive header");
419
+ path = record.slice(5);
420
+ rest = rest.subarray(length);
421
+ }
422
+ if (path === null) throw new Error("Unsafe Go archive header");
423
+ return path;
424
+ }
425
+
426
+ /** Members of a go.dev `.tar.gz`: POSIX ustar regular files and directories only. */
427
+ function goTarEntries(bytes) {
428
+ const tar = gunzipSync(bytes, { maxOutputLength: go.maxExpandedBytes });
429
+ const text = (block, start, length) => block.subarray(start, start + length).toString("utf8").replace(/\0[^]*$/, "");
430
+ const number = (block, start, length) => {
431
+ const value = text(block, start, length).trim();
432
+ if (!/^[0-7]{1,12}$/.test(value)) throw new Error("Unsafe Go archive header");
433
+ return parseInt(value, 8);
434
+ };
435
+ const entries = [];
436
+ let renamed = null;
437
+ for (let offset = 0; ;) {
438
+ if (offset + 512 > tar.length) throw new Error("Unsafe Go archive termination");
439
+ const block = tar.subarray(offset, offset + 512);
440
+ if (block.every((byte) => byte === 0)) {
441
+ if (renamed !== null || !tar.subarray(offset).every((byte) => byte === 0)) throw new Error("Unsafe Go archive termination");
442
+ return entries;
443
+ }
444
+ let checksum = 0;
445
+ for (let index = 0; index < 512; index += 1) checksum += index >= 148 && index < 156 ? 0x20 : block[index];
446
+ if (checksum !== number(block, 148, 8) || block.subarray(257, 265).toString("latin1") !== "ustar\u000000") throw new Error("Unsafe Go archive header");
447
+ const size = number(block, 124, 12);
448
+ const type = String.fromCharCode(block[156]);
449
+ const start = offset + 512;
450
+ if (start + size > tar.length) throw new Error("Unsafe Go archive termination");
451
+ offset = start + Math.ceil(size / 512) * 512;
452
+ if (type === "x" && renamed === null) {
453
+ renamed = paxPath(tar.subarray(start, start + size));
454
+ continue;
455
+ }
456
+ const directory = type === "5";
457
+ if (!directory && type !== "0" && type !== "\0") throw new Error("Unsafe Go archive entry type");
458
+ const prefix = text(block, 345, 155);
459
+ const name = renamed ?? (prefix ? `${prefix}/${text(block, 0, 100)}` : text(block, 0, 100));
460
+ renamed = null;
461
+ if ((directory && size !== 0) || (!directory && name.endsWith("/")) || text(block, 157, 100) !== "") throw new Error("Unsafe Go archive entry type");
462
+ entries.push({ name: goMember(name, directory), directory, executable: (number(block, 100, 8) & 0o111) !== 0,
463
+ bytes: tar.subarray(start, start + size) });
464
+ if (entries.length > 50000) throw new Error("Unsafe Go archive entry count");
465
+ }
466
+ }
467
+
468
+ /** Members of a go.dev Windows `.zip`: stored or deflated, unencrypted, no ZIP64,
469
+ * each matching its local header and CRC-32. A Unix-made member must be a
470
+ * regular file or directory (go.dev's are), never a link.
471
+ */
472
+ function goZipEntries(bytes) {
473
+ const fail = () => { throw new Error("Unsafe Go archive"); };
474
+ const end = bytes.length - 22;
475
+ if (end < 0 || bytes.readUInt32LE(end) !== 0x06054b50 || bytes.readUInt16LE(end + 20) !== 0) fail();
476
+ const count = bytes.readUInt16LE(end + 10);
477
+ const directoryStart = bytes.readUInt32LE(end + 16);
478
+ if (bytes.readUInt32LE(end + 4) !== 0 || bytes.readUInt16LE(end + 8) !== count || count > 50000 ||
479
+ directoryStart + bytes.readUInt32LE(end + 12) !== end) fail();
480
+ const entries = [];
481
+ let expanded = 0;
482
+ let at = directoryStart;
483
+ for (let index = 0; index < count; index += 1) {
484
+ if (at + 46 > end || bytes.readUInt32LE(at) !== 0x02014b50) fail();
485
+ const [flags, method, crc, compressed, size] = [bytes.readUInt16LE(at + 8), bytes.readUInt16LE(at + 10),
486
+ bytes.readUInt32LE(at + 16), bytes.readUInt32LE(at + 20), bytes.readUInt32LE(at + 24)];
487
+ const nameLength = bytes.readUInt16LE(at + 28);
488
+ const mode = bytes.readUInt32LE(at + 38) >>> 16;
489
+ const local = bytes.readUInt32LE(at + 42);
490
+ const name = bytes.subarray(at + 46, at + 46 + nameLength).toString("utf8");
491
+ const directory = name.endsWith("/");
492
+ const unix = bytes[at + 5] === 3;
493
+ at += 46 + nameLength + bytes.readUInt16LE(at + 30) + bytes.readUInt16LE(at + 32);
494
+ if (at > end || (flags & 0x41) !== 0 || (method !== 0 && method !== 8) || compressed === 0xffffffff || size === 0xffffffff) fail();
495
+ if (unix && (mode & 0o170000) !== (directory ? 0o040000 : 0o100000)) fail();
496
+ if (local + 30 > directoryStart || bytes.readUInt32LE(local) !== 0x04034b50) fail();
497
+ const data = local + 30 + bytes.readUInt16LE(local + 26) + bytes.readUInt16LE(local + 28);
498
+ if (bytes.subarray(local + 30, local + 30 + bytes.readUInt16LE(local + 26)).toString("utf8") !== name || data + compressed > directoryStart) fail();
499
+ expanded += size;
500
+ if (expanded > go.maxExpandedBytes || (directory && size !== 0)) fail();
501
+ const raw = bytes.subarray(data, data + compressed);
502
+ const content = method === 0 ? raw : inflateRawSync(raw, { maxOutputLength: Math.max(size, 1) });
503
+ if (content.length !== size || crc32(content) !== crc) fail();
504
+ entries.push({ name: goMember(name, directory), directory, executable: unix && (mode & 0o111) !== 0, bytes: content });
505
+ }
506
+ if (at !== end) fail();
507
+ return entries;
508
+ }
509
+
510
+ const GO_MARKER = ".gentle-shell-go";
511
+ /** An existing directory owned by this user that no one else can write to. */
512
+ function privateDirectory(path) {
513
+ const info = stat(path);
514
+ if (!info?.isDirectory() || info.isSymbolicLink()) return false;
515
+ return process.platform === "win32" || (info.uid === process.getuid() && (info.mode & 0o022) === 0);
516
+ }
517
+
518
+ /** The pinned Go a previous acquireGo published under `root` for this target, or
519
+ * null: `<root>/<version>` private, marked with the exact pinned archive URL,
520
+ * holding a regular `go/bin/go` (`go.exe` on Windows). Read-only; never throws.
521
+ */
522
+ export function installedGo(root, platform, arch, adapters = {}) {
523
+ try {
524
+ const descriptor = (adapters.artifact ?? artifactFor)("go", platform, arch);
525
+ const directory = join(root, descriptor.version);
526
+ const goPath = join(directory, "go", "bin", platform === "win32" ? "go.exe" : "go");
527
+ if (!privateDirectory(root) || !privateDirectory(directory) || !regular(join(directory, GO_MARKER))) return null;
528
+ return readFileSync(join(directory, GO_MARKER), "utf8") === `${descriptor.url}\n` && regular(goPath) ? goPath : null;
529
+ } catch {
530
+ return null;
531
+ }
532
+ }
533
+
534
+ /** The pinned Go, only to build Gentle AI: the copy installedGo finds, otherwise
535
+ * the exact go.dev archive, verified (size and SHA-256) before it is read,
536
+ * extracted in process into a private staging directory (regular files and
537
+ * directories under `go/` only; no links, traversal or duplicates), checked
538
+ * against its VERSION, marked, and published without replacing anything as
539
+ * `<root>/<version>/go` by one rename of the staging directory, so the
540
+ * destination never exists unmarked. Nothing outside `root` is written, and the
541
+ * user's own Go, PATH and profile are never touched. Adapters (artifact,
542
+ * download, digest) are trusted local test code only.
543
+ * Returns { goPath, version, acquired }.
544
+ */
545
+ export async function acquireGo({ root, platform, arch, adapters = {} }) {
546
+ const descriptor = (adapters.artifact ?? artifactFor)("go", platform, arch);
547
+ const executable = platform === "win32" ? "go.exe" : "go";
548
+ const destination = join(root, descriptor.version);
549
+ const reused = installedGo(root, platform, arch, adapters);
550
+ if (reused) return { goPath: reused, version: descriptor.version, acquired: false };
551
+ let stage = null;
552
+ try {
553
+ // Never replaced: an unmarked folder may not be the installer's.
554
+ if (stat(destination)) throw new Error(`Conflicting Go destination: ${destination}`);
555
+ const bytes = await verifiedDownload("go", adapters, platform, arch);
556
+ mkdirSync(root, { recursive: true, mode: 0o700 });
557
+ if (!privateDirectory(root)) throw new Error("Unsafe Go destination");
558
+ const entries = descriptor.url.endsWith(".zip") ? goZipEntries(bytes) : goTarEntries(bytes);
559
+ stage = mkdtempSync(join(root, ".stage-"));
560
+ for (const entry of entries) {
561
+ const target = join(stage, ...entry.name.split("/"));
562
+ if (entry.directory) {
563
+ mkdirSync(target, { recursive: true, mode: 0o755 });
564
+ } else {
565
+ mkdirSync(dirname(target), { recursive: true, mode: 0o755 });
566
+ // wx: a duplicate member, also one differing only in case, fails closed.
567
+ writeFileSync(target, entry.bytes, { flag: "wx", mode: entry.executable ? 0o755 : 0o644 });
568
+ }
569
+ }
570
+ const tree = join(stage, "go");
571
+ if (!regular(join(tree, "bin", executable)) || !regular(join(tree, "VERSION")) ||
572
+ readFileSync(join(tree, "VERSION"), "utf8").split("\n")[0] !== `go${descriptor.version}`) throw new Error("Go archive content rejected");
573
+ // The staging directory becomes the destination whole: marked first, then
574
+ // one rename (an existing non-empty destination makes it fail).
575
+ writeFileSync(join(stage, GO_MARKER), `${descriptor.url}\n`, { flag: "wx", mode: 0o600 });
576
+ if (stat(destination)) throw new Error(`Conflicting Go destination: ${destination}`);
577
+ renameSync(stage, destination);
578
+ stage = null;
579
+ return { goPath: join(destination, "go", "bin", executable), version: descriptor.version, acquired: true };
580
+ } catch (cause) {
581
+ throw new Error("Go verified acquisition failed; nothing was published", { cause });
582
+ } finally {
583
+ if (stage) rmSync(stage, { recursive: true, force: true });
584
+ }
585
+ }
586
+
587
+ /** No shell command or URL from the caller is executed: entry is fixed in bundle. */
588
+ export async function launchWizard({ bundle, env }) {
589
+ const entry = join(bundle, "bin/gentle-shell-install.mjs");
590
+ if (!regular(entry)) throw new Error("Future wizard entry is missing; no live wizard is available");
591
+ await new Promise((resolveChild, reject) => {
592
+ const child = spawn(process.execPath, [entry], { env, stdio: "inherit", shell: false });
593
+ child.once("error", () => reject(new Error("Wizard child could not start")));
594
+ child.once("exit", (code) => code === 0 ? resolveChild() : reject(new Error("Wizard child failed")));
595
+ });
596
+ }
597
+
598
+ /** `env` and transport/process adapters are injectable for local fixtures only. */
599
+ export async function bootstrap(bundle, suppliedTools, { env = process.env, adapters = {} } = {}) {
600
+ const entry = join(bundle, "bin/gentle-shell-install.mjs");
601
+ if (!regular(entry)) throw new Error("Future wizard entry is missing");
602
+ const metadata = JSON.parse(readFileSync(join(bundle, "package.json"), "utf8"));
603
+ if (!compatibleEngine(metadata.engines?.node, process.versions.node)) throw new Error("Node requirement rejected");
604
+ if (metadata.packageManager !== `pnpm@${pnpm.version}`) throw new Error("pnpm acquisition pin differs from repository; update verified descriptors first");
605
+ let tools = suppliedTools;
606
+ let created = false;
607
+ // Private tools are claimed only when pnpm is acquired (missing or incompatible).
608
+ const claim = () => {
609
+ if (!tools) { tools = privateTools(env.HOME); created = true; }
610
+ return tools;
611
+ };
612
+ try {
613
+ const result = await ensurePnpm({ tools: claim, env, nodeVersion: process.versions.node, adapters });
614
+ await launchWizard({ bundle, env: result.env });
615
+ } catch (error) {
616
+ if (created) rmSync(tools, { recursive: true, force: true });
617
+ throw error;
618
+ }
619
+ // Exit 0 means pnpm now persists under $PNPM_HOME or was already the user's.
620
+ if (created && !removeOwnedTools(tools, env.HOME)) {
621
+ console.error(`Bootstrap: installation finished, but temporary tools could not be removed: ${tools}`);
622
+ }
623
+ }
624
+ /** The Windows helper failure line appends only a fixed-shape reason code. Any
625
+ * other value, including a failed module import, reports `unexpected-helper`.
626
+ */
627
+ export function windowsBootstrapMessage(error) {
628
+ const reason = typeof error?.reason === "string" && /^(?:[a-z][a-z-]* \([a-z][a-z-]*\)|unexpected-[a-z][a-z-]*)$/.test(error.reason) ? error.reason : "unexpected-helper";
629
+ return `Windows bootstrap failed; policy, prerequisite or bundle evidence rejected. No installation completed. Reason: ${reason}`;
630
+ }
631
+ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
632
+ if (process.argv.length === 5 && process.argv[2] === "--bootstrap-windows") {
633
+ import("./installer-windows.mjs").then(({ bootstrapWindows }) => bootstrapWindows({ bundle: process.argv[3], tools: process.argv[4], env: process.env })).catch((error) => {
634
+ console.error(windowsBootstrapMessage(error));
635
+ process.exitCode = 1;
636
+ });
637
+ } else if (process.argv.length !== 5 || process.argv[2] !== "--bootstrap") {
638
+ console.error("Use the bundle's POSIX bootstrap script");
639
+ process.exitCode = 1;
640
+ } else {
641
+ bootstrap(process.argv[3], process.argv[4]).catch((error) => {
642
+ // Only controlled errors are emitted; never print URL/process adapter errors.
643
+ const safe = /^(Future wizard|Node requirement|pnpm |Existing pnpm|Unknown prerequisite|Unsafe tooling|Wizard child|Prerequisite process|Required utility)/.test(error.message);
644
+ console.error(safe ? error.message : "Prerequisite bootstrap failed");
645
+ process.exitCode = 1;
646
+ });
647
+ }
648
+ }