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,564 @@
1
+ import { spawn as spawnProcess } from "node:child_process";
2
+ import { constants } from "node:fs";
3
+ import { access, lstat, readFile, realpath, stat } from "node:fs/promises";
4
+ import { posix, win32 } from "node:path";
5
+ import { gentleAiBinaryPath } from "../runtime/gentle-ai-binary.mjs";
6
+ import { pnpmGlobalBin, requirements } from "./installer-preflight.mjs";
7
+ import { installOwner } from "./main-channel.mjs";
8
+ import {
9
+ PI_PACKAGE,
10
+ SHELL_PACKAGE,
11
+ childEnvironment,
12
+ contains,
13
+ genuineNpm,
14
+ lookPath,
15
+ npmInvocation,
16
+ packageNativeGentleAi,
17
+ pnpmInvocation,
18
+ recoverableStackRoot,
19
+ samePath,
20
+ spawnable,
21
+ succeeded,
22
+ windowsInvocation,
23
+ } from "./installer-runner.mjs";
24
+ import { verifyWindowsStorage, verifyWindowsStorageMany } from "./installer-windows.mjs";
25
+
26
+ // Real host probes for collectInventory. Every effect goes through injected
27
+ // adapters; probes only run fixed read-only argv (`--version`, `go version`,
28
+ // `pnpm list -g`, `npm config get prefix`, `npm root -g`) and never write,
29
+ // create directories or run setup/postinstall.
30
+
31
+ const SECOND = 1000;
32
+ const deadlines = Object.freeze({ version: 10 * SECOND, list: 30 * SECOND });
33
+ const BOOTSTRAP_TOOLS = /^\.gentle-shell-bootstrap-tools\./;
34
+
35
+ const unknown = () => ({ available: null });
36
+ const absent = () => ({ available: false });
37
+
38
+ function pathOf(platform) {
39
+ return platform === "win32" ? win32 : posix;
40
+ }
41
+ function pathKey(env, platform) {
42
+ return platform === "win32" ? Object.keys(env).find((key) => key.toUpperCase() === "PATH") ?? "Path" : "PATH";
43
+ }
44
+ function envValue(env, name, platform) {
45
+ const key = platform === "win32" ? Object.keys(env).find((candidate) => candidate.toUpperCase() === name) : name;
46
+ return key === undefined ? undefined : env[key];
47
+ }
48
+ function plainObject(value) {
49
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
50
+ }
51
+ function exactVersion(text, pattern) {
52
+ const match = pattern.exec(String(text ?? "").trim());
53
+ return match ? `${match[1]}.${match[2]}.${match[3] ?? "0"}` : null;
54
+ }
55
+ const NUMBER = "(0|[1-9]\\d*)";
56
+ const STABLE = new RegExp(`^${NUMBER}\\.${NUMBER}\\.${NUMBER}$`);
57
+ // A main-channel Gentle Shell (scripts/main-channel.mjs): <stable>-main.<sha12>.
58
+ const MAIN_BUILD = new RegExp(`^${NUMBER}\\.${NUMBER}\\.${NUMBER}-main\\.[0-9a-f]{12}$`);
59
+ const NODE_VERSION = new RegExp(`^v${NUMBER}\\.${NUMBER}\\.${NUMBER}$`);
60
+ const GO_VERSION = new RegExp(`^go version go${NUMBER}\\.${NUMBER}(?:\\.${NUMBER})? \\S+$`);
61
+ function atLeast(version, minimum) {
62
+ const [left, right] = [version, minimum].map((value) => value.split(".").map(Number));
63
+ for (let i = 0; i < 3; i += 1) if (left[i] !== right[i]) return left[i] > right[i];
64
+ return true;
65
+ }
66
+
67
+ function normalDirectory(path, value) {
68
+ return path.normalize(value).replace(/(.)[\\/]+$/, "$1");
69
+ }
70
+ function sameDirectory(platform, left, right) {
71
+ const path = pathOf(platform);
72
+ const [a, b] = [normalDirectory(path, left), normalDirectory(path, right)];
73
+ return platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
74
+ }
75
+
76
+ /** Bootstrap tool roots: `GENTLE_BOOTSTRAP_TOOLS` and every PATH entry segment
77
+ * named `.gentle-shell-bootstrap-tools.*` (the bootstrap's temporary directory).
78
+ */
79
+ export function bootstrapRoots({ platform, env }) {
80
+ const path = pathOf(platform);
81
+ const roots = [];
82
+ const add = (root) => {
83
+ const normal = normalDirectory(path, root);
84
+ if (!roots.some((other) => sameDirectory(platform, other, normal))) roots.push(normal);
85
+ };
86
+ const tools = envValue(env, "GENTLE_BOOTSTRAP_TOOLS", platform);
87
+ if (typeof tools === "string" && path.isAbsolute(tools)) add(tools);
88
+ const marker = platform === "win32" ? new RegExp(BOOTSTRAP_TOOLS.source, "i") : BOOTSTRAP_TOOLS;
89
+ for (const entry of String(env[pathKey(env, platform)] ?? "").split(path.delimiter)) {
90
+ if (!path.isAbsolute(entry)) continue;
91
+ const parts = path.normalize(entry).split(path.sep);
92
+ const index = parts.findIndex((part) => marker.test(part));
93
+ if (index > 0) add(parts.slice(0, index + 1).join(path.sep) || path.sep);
94
+ }
95
+ return roots;
96
+ }
97
+
98
+ /** The user's own environment: PATH without bootstrap tool directories. */
99
+ export function userEnvironment({ platform, env }) {
100
+ const path = pathOf(platform);
101
+ const roots = bootstrapRoots({ platform, env });
102
+ const key = pathKey(env, platform);
103
+ if (env[key] === undefined) return { ...env };
104
+ const own = (entry) => !roots.some((root) => sameDirectory(platform, root, entry) || contains(root, entry, platform));
105
+ return { ...env, [key]: String(env[key]).split(path.delimiter).filter((entry) => entry.length > 0 && own(entry)).join(path.delimiter) };
106
+ }
107
+
108
+ /** Real adapters: argv spawn without a shell, SIGKILL at the deadline, bounded
109
+ * stdout (stderr discarded: it can hold private paths) and read-only fs checks.
110
+ * Only a caller that passes `stderrTail: <bytes>` (a positive integer, clamped
111
+ * to 4 KiB) gets stderr piped; the result then carries just its last bytes as
112
+ * `stderrTail`. Callers must sanitize that text before showing it anywhere.
113
+ * The deadline signals the direct child only, not its descendants, and settles
114
+ * the result as timed out without waiting for the child's pipes to close.
115
+ * An optional `cwd` sets the child's working directory.
116
+ * `spawn` is injectable only for trusted tests.
117
+ */
118
+ export function hostAdapters({ maxOutputBytes = 1024 * 1024, maxTextBytes = 1024 * 1024, spawn = spawnProcess } = {}) {
119
+ const run = (command, argv, { env, cwd, deadlineMs, stderrTail }) => new Promise((resolve) => {
120
+ let size = 0;
121
+ let truncated = false;
122
+ let timedOut = false;
123
+ const chunks = [];
124
+ const tailBytes = Number.isSafeInteger(stderrTail) && stderrTail > 0 ? Math.min(stderrTail, 4096) : 0;
125
+ let tail = Buffer.alloc(0);
126
+ const withTail = (result) => (tailBytes > 0 ? { ...result, stderrTail: tail.toString("utf8") } : result);
127
+ let child;
128
+ try {
129
+ child = spawn(command, argv, { env, cwd, shell: false, stdio: ["ignore", "pipe", tailBytes > 0 ? "pipe" : "ignore"], windowsHide: true });
130
+ } catch {
131
+ resolve(withTail({ code: null, signal: null, timedOut: false, truncated: false, stdout: "" }));
132
+ return;
133
+ }
134
+ let settled = false;
135
+ const settle = (code, signal) => {
136
+ if (settled) return;
137
+ settled = true;
138
+ clearTimeout(timer);
139
+ resolve(withTail({ code, signal, timedOut, truncated, stdout: Buffer.concat(chunks).toString("utf8") }));
140
+ };
141
+ // Keep only the last bytes: a long setup log never accumulates in memory.
142
+ if (tailBytes > 0) child.stderr?.on("data", (chunk) => {
143
+ const joined = Buffer.concat([tail, chunk]);
144
+ tail = joined.subarray(Math.max(0, joined.length - tailBytes));
145
+ });
146
+ // A descendant can keep stdout open after the kill, so `close` may never come:
147
+ // settle at the deadline and release the pipes instead of waiting for it.
148
+ const timer = setTimeout(() => {
149
+ timedOut = true;
150
+ child.kill("SIGKILL");
151
+ for (const stream of [child.stdin, child.stdout, child.stderr]) stream?.destroy();
152
+ settle(null, "SIGKILL");
153
+ }, deadlineMs);
154
+ child.stdout.on("data", (chunk) => {
155
+ const room = maxOutputBytes - size;
156
+ if (chunk.length > room) truncated = true;
157
+ if (room > 0) {
158
+ chunks.push(chunk.subarray(0, room));
159
+ size += Math.min(room, chunk.length);
160
+ }
161
+ });
162
+ child.once("error", () => settle(null, null));
163
+ child.once("close", settle);
164
+ });
165
+ const info = (path, follow = true) => (follow ? stat(path) : lstat(path)).catch((error) => error);
166
+ const fs = {
167
+ isFile: async (path) => (await stat(path).catch(() => null))?.isFile() === true,
168
+ isDirectory: async (path) => (await stat(path).catch(() => null))?.isDirectory() === true,
169
+ // Only a missing entry is absent; an unreadable one exists (and is then not a writable directory).
170
+ exists: async (path) => {
171
+ const result = await info(path, false);
172
+ return !(result instanceof Error) || !["ENOENT", "ENOTDIR"].includes(result.code);
173
+ },
174
+ realpath: (path) => realpath(path),
175
+ readText: async (path) => {
176
+ const result = await stat(path);
177
+ if (!result.isFile() || result.size > maxTextBytes) throw new Error("Unreadable probe file");
178
+ return readFile(path, "utf8");
179
+ },
180
+ writable: (path) => access(path, constants.W_OK).then(() => true, () => false),
181
+ // A symbolic link, or on Windows a junction (libuv's lstat reports both as links).
182
+ // A missing entry is no link; any other error throws.
183
+ isLink: async (path) => {
184
+ const result = await info(path, false);
185
+ if (!(result instanceof Error)) return result.isSymbolicLink();
186
+ if (["ENOENT", "ENOTDIR"].includes(result.code)) return false;
187
+ throw result;
188
+ },
189
+ };
190
+ return { run, fs };
191
+ }
192
+
193
+ /**
194
+ * createProbes({ platform, env, run, fs, home?, verifyGentleAi?, storage?, storageMany?, pnpmHome? }) -> the eight
195
+ * named collectInventory probes, plus `folders` (Windows only). `env` is the wizard's environment (bootstrap
196
+ * tools first on PATH); `run` and `fs` follow hostAdapters. Each probe returns
197
+ * the shape collectInventory documents, or { available: null } when unknown or
198
+ * failed; errors are never thrown or retained. On Windows, `storage(file)` walks a
199
+ * file the way the bootstrap does (verifyWindowsStorage by default), `storageMany(files)`
200
+ * walks many in one launch (verifyWindowsStorageMany) and `pnpmHome` is the
201
+ * wizard's PNPM_HOME decision (windowsPnpmHome).
202
+ */
203
+ export function createProbes({ platform, env, run, fs, home, verifyGentleAi = packageNativeGentleAi,
204
+ storage = platform === "win32" ? (file) => verifyWindowsStorage(file, env) : null,
205
+ storageMany = platform === "win32" ? (files) => verifyWindowsStorageMany(files, env) : null, pnpmHome = null }) {
206
+ const path = pathOf(platform);
207
+ const user = userEnvironment({ platform, env });
208
+ const globalBin = pnpmGlobalBin({ platform, env: user });
209
+ // pnpm 11 global commands need `$PNPM_HOME/bin` on PATH, as in the runner.
210
+ const privateHome = platform === "win32" && pnpmHome?.source === "private";
211
+ const child = globalBin ? childEnvironment(env, platform, globalBin, { privateHome }) : env;
212
+ const userChild = globalBin ? childEnvironment(user, platform, globalBin, { privateHome }) : user;
213
+ /** Every file passes the storage walk; any failure means it is not usable. */
214
+ const trusted = async (files) => {
215
+ if (!storage) return true;
216
+ try {
217
+ for (const file of files) await storage(file);
218
+ return true;
219
+ } catch {
220
+ return false;
221
+ }
222
+ };
223
+ const userHome = home ?? (platform === "win32" ? env.USERPROFILE : env.HOME);
224
+ const output = async (command, argv, runEnv, deadlineMs) => {
225
+ const result = await run(command, argv, { env: runEnv, deadlineMs });
226
+ return succeeded(result) && result.truncated !== true ? String(result.stdout ?? "").trim() : null;
227
+ };
228
+ const persistentOn = async (name) => (await lookPath(name, user, platform, fs)) !== null;
229
+ const roots = bootstrapRoots({ platform, env });
230
+ const marker = platform === "win32" ? new RegExp(BOOTSTRAP_TOOLS.source, "i") : BOOTSTRAP_TOOLS;
231
+ /** A file inside the bootstrap's temporary tools directory. */
232
+ const fromBootstrap = (file) => path.isAbsolute(file) && (roots.some((root) => contains(root, file, platform)) ||
233
+ path.normalize(file).split(path.sep).some((part) => marker.test(part)));
234
+
235
+ let listing;
236
+ /** Output of the single `list -g` call, or null when it is unavailable. */
237
+ const globalListing = () => (listing ??= (async () => {
238
+ // A private PNPM_HOME this run has not created yet holds nothing; listing it
239
+ // could make pnpm create it, unprotected, before consent.
240
+ if (privateHome && globalBin && !(await fs.exists(globalBin.pnpmHome))) return "[]";
241
+ const pnpm = globalBin ? await pnpmInvocation(env, platform, fs) : null;
242
+ if (!pnpm) return null;
243
+ return output(pnpm.command, [...pnpm.prefix, "list", "-g", "--depth", "0", "--json"], child, deadlines.list);
244
+ })());
245
+ /** pnpm-global packages by name from that listing; null entries are ambiguous. */
246
+ const globalPackages = async () => {
247
+ const stdout = await globalListing();
248
+ if (stdout === null) return null;
249
+ const projects = JSON.parse(stdout);
250
+ if (!Array.isArray(projects)) return null;
251
+ const packages = new Map();
252
+ for (const project of projects) {
253
+ if (!plainObject(project)) return null;
254
+ for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
255
+ const dependencies = project[field];
256
+ if (dependencies === undefined) continue;
257
+ if (!plainObject(dependencies)) return null;
258
+ for (const [name, entry] of Object.entries(dependencies)) {
259
+ packages.set(name, packages.has(name) || !plainObject(entry) ? null : entry);
260
+ }
261
+ }
262
+ }
263
+ return packages;
264
+ };
265
+ /** A package that is not pnpm-global but whose command resolves elsewhere is unknown, never absent. */
266
+ const globalPackage = async (name, command) => {
267
+ const packages = await globalPackages();
268
+ if (!packages) return { state: "unknown" };
269
+ if (!packages.has(name)) return (await persistentOn(command)) ? { state: "unknown", outsidePnpm: true } : { state: "absent" };
270
+ const entry = packages.get(name);
271
+ const version = exactVersion(entry?.version, STABLE);
272
+ // A main-channel Shell (<stable>-main.<sha12>) is present too: it can be updated.
273
+ const main = typeof entry?.version === "string" && MAIN_BUILD.test(entry.version) ? entry.version : null;
274
+ return entry && (version || main) ? { state: "present", entry, version: version ?? main } : { state: "unknown" };
275
+ };
276
+ // Still unknown (never absent or replaced), but says the command comes from
277
+ // another installation so the wizard can explain the blocker.
278
+ const notPnpmGlobal = (found) => (found.outsidePnpm ? { ...unknown(), outsidePnpm: true } : unknown());
279
+ /** How a command found on the user's PATH runs: as it is on POSIX, through
280
+ * windowsInvocation (never cmd.exe) on Windows. Null when it cannot run.
281
+ */
282
+ const invocation = (file) => (platform === "win32" ? windowsInvocation(file, user, { run, fs }) : { command: file, prefix: [] });
283
+ /** The package a command on the user's PATH runs from: its real root and version.
284
+ * With `followShim`, a Windows shim is followed to the entry it runs.
285
+ */
286
+ const packageOnPath = async (command, name, followShim = false) => {
287
+ const found = await lookPath(command, user, platform, fs);
288
+ if (!found) return null;
289
+ let start = found;
290
+ if (followShim && platform === "win32") {
291
+ const target = await invocation(found);
292
+ if (!target) return null;
293
+ start = target.prefix[0] ?? target.command;
294
+ }
295
+ let directory = path.dirname(await fs.realpath(start));
296
+ for (let depth = 0; depth < 6; depth += 1) {
297
+ const text = await fs.readText(path.join(directory, "package.json")).catch(() => null);
298
+ if (text !== null) {
299
+ let manifest = null;
300
+ try { manifest = JSON.parse(text); } catch { manifest = null; }
301
+ if (manifest?.name === name && typeof manifest.version === "string") return { root: directory, version: manifest.version };
302
+ }
303
+ const parent = path.dirname(directory);
304
+ if (parent === directory) break;
305
+ directory = parent;
306
+ }
307
+ return null;
308
+ };
309
+ /** Where `file` really is (realpath) and, for every link on its path (lstat:
310
+ * a symbolic link or junction), the real folder that holds that link: whoever
311
+ * can change that folder can retarget the link. A realpath that merely spells a
312
+ * component differently, such as an 8.3 short name (RUNNER~1), is no link.
313
+ * Null when the real location, or whether a component is a link, cannot be read.
314
+ */
315
+ const canonicalFiles = async (file) => {
316
+ const real = await fs.realpath(file).catch(() => null);
317
+ if (real === null) return null;
318
+ const result = [real];
319
+ for (let current = file; path.dirname(current) !== current; current = path.dirname(current)) {
320
+ let link;
321
+ try {
322
+ link = (await fs.isLink?.(current)) === true;
323
+ } catch {
324
+ return null;
325
+ }
326
+ if (!link) continue;
327
+ const holder = await fs.realpath(path.dirname(current)).catch(() => null);
328
+ if (holder === null) return null;
329
+ if (!result.some((entry) => samePath(entry, holder, platform))) result.push(holder);
330
+ }
331
+ return result;
332
+ };
333
+ let npmRoot;
334
+ /** `npm root -g`, real path, from the user's npm (npmInvocation). */
335
+ const npmGlobalRoot = () => (npmRoot ??= (async () => {
336
+ const npm = await npmInvocation(user, platform, { run, fs });
337
+ const stdout = npm ? await output(npm.command, [...npm.prefix, "root", "-g"], user, deadlines.version) : null;
338
+ const line = stdout?.split(/\r?\n/).at(-1)?.trim() ?? "";
339
+ return path.isAbsolute(line) ? fs.realpath(line).catch(() => null) : null;
340
+ })());
341
+ const shellBin = () => path.join(globalBin.path, platform === "win32" ? "gentle-shell.cmd" : "gentle-shell");
342
+ /** The Pi on the user's PATH outside pnpm: real root, its package version and owner (npm or null). */
343
+ const piOnPath = async () => {
344
+ const found = await packageOnPath("pi", PI_PACKAGE, true);
345
+ if (!found) return null;
346
+ return { root: found.root, version: found.version,
347
+ owner: installOwner({ packageRoot: found.root, pnpmHome: null, npmRoot: await npmGlobalRoot(), name: PI_PACKAGE, platform }) };
348
+ };
349
+
350
+ const probes = {
351
+ /** Windows (S6 notice): the folders of the tools the user's PATH resolves (Node,
352
+ * npm, Go, Pi, Gentle Shell), each command and what it runs, walked in one
353
+ * storageMany launch where they really are (canonicalFiles): links such as
354
+ * pnpm's global `node_modules\<pkg>` junction or a `pnpm runtime` node.exe are
355
+ * followed, never reported, and the folder holding each link is walked too.
356
+ * A real location that is not on a local drive (a mapped network drive, or a
357
+ * link to a UNC share) is never walked: it could not be checked, for its tool
358
+ * only. { node?, npm?, go?, pi?, shell? }: per tool the first concrete finding,
359
+ * else the first path that could not be checked ({ check: "unchecked", at });
360
+ * null when nothing fails or the walk cannot finish.
361
+ */
362
+ async folders() {
363
+ if (!storageMany) return null;
364
+ const files = {};
365
+ const unchecked = new Map();
366
+ for (const [tool, command] of [["node", "node"], ["npm", "npm"], ["go", "go"], ["pi", "pi"], ["shell", "gentle-shell"]]) {
367
+ const found = await lookPath(command, user, platform, fs);
368
+ if (!found) continue;
369
+ const runs = await Promise.resolve().then(() => invocation(found)).catch(() => null);
370
+ files[tool] = [];
371
+ for (const file of [found, ...(runs ? [runs.command, ...runs.prefix] : [])]) {
372
+ const canonical = await canonicalFiles(file);
373
+ if (canonical === null) unchecked.set(file.toLowerCase(), { check: "unchecked", at: file });
374
+ for (const real of canonical ?? []) if (!/^[A-Za-z]:\\/.test(real)) unchecked.set(real.toLowerCase(), { check: "unchecked", at: real });
375
+ files[tool].push(...(canonical ?? [file]));
376
+ }
377
+ }
378
+ const key = (file) => file.toLowerCase();
379
+ const paths = [...new Map(Object.values(files).flat().filter((file) => !unchecked.has(key(file))).map((file) => [key(file), file])).values()];
380
+ if (paths.length === 0 && unchecked.size === 0) return null;
381
+ let results;
382
+ try {
383
+ results = paths.length === 0 ? [] : await storageMany(paths);
384
+ } catch {
385
+ return null;
386
+ }
387
+ const byPath = new Map([...unchecked, ...paths.map((file, index) => [key(file), results[index] ?? null])]);
388
+ const findings = Object.fromEntries(Object.entries(files).map(([tool, list]) => {
389
+ const found = list.map((file) => byPath.get(key(file))).filter(Boolean);
390
+ return [tool, found.find((finding) => finding.check !== "unchecked") ?? found[0]];
391
+ }).filter(([, finding]) => finding));
392
+ return Object.keys(findings).length > 0 ? findings : null;
393
+ },
394
+ /** The installed Gentle Shell for an update: real root, version and owner (pnpm, npm or null). */
395
+ async locateShell() {
396
+ const shell = await globalPackage(SHELL_PACKAGE, "gentle-shell");
397
+ if (shell.state === "present" && typeof shell.entry.path === "string" && path.isAbsolute(shell.entry.path)) {
398
+ return { root: await fs.realpath(shell.entry.path), version: shell.version, owner: "pnpm" };
399
+ }
400
+ if (shell.state !== "unknown" || !shell.outsidePnpm) return null;
401
+ const found = await packageOnPath("gentle-shell", SHELL_PACKAGE, true);
402
+ if (!found) return null;
403
+ return { root: found.root, version: found.version, owner: installOwner({ packageRoot: found.root, pnpmHome: null, npmRoot: await npmGlobalRoot(), platform }) };
404
+ },
405
+ /** The single installed Pi for an update: real root, version and owner (pnpm, npm or null). */
406
+ async locatePi() {
407
+ const pi = await globalPackage(PI_PACKAGE, "pi");
408
+ if (pi.state === "present" && typeof pi.entry.path === "string" && path.isAbsolute(pi.entry.path)) {
409
+ return { root: await fs.realpath(pi.entry.path), version: pi.version, owner: "pnpm" };
410
+ }
411
+ return pi.state === "unknown" && pi.outsidePnpm ? piOnPath() : null;
412
+ },
413
+ async node() {
414
+ const persistent = await lookPath("node", user, platform, fs);
415
+ const bootstrap = await lookPath("node", env, platform, fs);
416
+ let node = persistent ?? bootstrap;
417
+ if (!node) return absent();
418
+ if (!spawnable(node, platform)) return unknown();
419
+ const nodeVersion = async (file) => exactVersion(await output(file, ["--version"], env, deadlines.version), NODE_VERSION);
420
+ let version = await nodeVersion(node);
421
+ if (!version) return unknown();
422
+ // An older Node on the user's PATH is left as it is: the bootstrap's verified
423
+ // Node runs the installer, so it is reported (bootstrap-only) with the older one found.
424
+ let found = null;
425
+ if (persistent && bootstrap && persistent !== bootstrap && !atLeast(version, requirements.node) && spawnable(bootstrap, platform)) {
426
+ const pinned = await nodeVersion(bootstrap);
427
+ if (pinned && atLeast(pinned, requirements.node)) [found, node, version] = [version, bootstrap, pinned];
428
+ }
429
+ // A usable npm must resolve without bootstrap tools, as in a fresh terminal.
430
+ let npm = null;
431
+ try {
432
+ npm = typeof (await genuineNpm(userChild, platform, node, { run, fs }, globalBin)) === "object";
433
+ } catch {
434
+ npm = null;
435
+ }
436
+ return { available: true, version, usable: true, persistent: persistent !== null && found === null, npm, ...(found ? { found } : {}) };
437
+ },
438
+ async pnpm() {
439
+ const pnpm = await pnpmInvocation(env, platform, fs);
440
+ // Without the direct handoff a Windows .cmd shim cannot run with shell:false.
441
+ if (!pnpm) return (await lookPath("pnpm", env, platform, fs)) ? unknown() : absent();
442
+ const version = exactVersion(await output(pnpm.command, [...pnpm.prefix, "--version"], child, deadlines.version), STABLE);
443
+ if (!version) return unknown();
444
+ // pnpm checks its Node engine at startup. Any major at the pin or newer: the
445
+ // runner's commands were run with pnpm 12 against a pnpm 11 global home and back.
446
+ const compatible = atLeast(version, requirements.pnpm);
447
+ // The bootstrap's own pnpm (POSIX PATH or Windows handoff entry) is bootstrap-only
448
+ // even next to the user's pnpm, which it acquired because that one is incompatible.
449
+ if (!fromBootstrap(pnpm.prefix[0] ?? pnpm.command)) {
450
+ return { available: true, version, usable: true, compatible, persistent: await persistentOn("pnpm") };
451
+ }
452
+ // That pnpm's version, read in the user's environment. On Windows only when its
453
+ // shim and what it runs pass the storage walk: an untrusted pnpm never runs.
454
+ const own = await lookPath("pnpm", user, platform, fs);
455
+ const ownPnpm = own ? await invocation(own) : null;
456
+ const runnable = ownPnpm !== null && await trusted([own, ownPnpm.command, ...ownPnpm.prefix]);
457
+ const result = runnable ? await run(ownPnpm.command, [...ownPnpm.prefix, "--version"], { env: user, cwd: path.parse(own).root, deadlineMs: deadlines.version }) : null;
458
+ const found = succeeded(result) && result.truncated !== true ? exactVersion(result.stdout, STABLE) : null;
459
+ const usableFound = found !== null && atLeast(found, requirements.pnpm);
460
+ // Persisting pnpm writes $PNPM_HOME/bin: a user's pnpm there with a stable version
461
+ // is reported as it is (an incompatible one blocks), never replaced or downgraded.
462
+ // One that fails the walk (untrusted), or whose shim is not recognized or whose
463
+ // version cannot be read (unchecked), is never run again: the bootstrap's pnpm is
464
+ // reported in its place, as persistent, so nothing is persisted over the user's.
465
+ if (own && globalBin && samePath(path.dirname(own), globalBin.path, platform)) {
466
+ if (found) return { available: true, version: found, usable: true, compatible: usableFound, persistent: true, inGlobalBin: true };
467
+ return { available: true, version, usable: true, compatible, persistent: true, inGlobalBin: true,
468
+ ...(ownPnpm !== null && !runnable ? { untrusted: true } : { unchecked: true }) };
469
+ }
470
+ // The bootstraps run only the pinned major (installer-downloads pinnedPnpmCompatible): a pnpm
471
+ // of another major beside them is reported, so the plan says it is left unchanged.
472
+ const sameMajor = found !== null && found.split(".")[0] === requirements.pnpm.split(".")[0];
473
+ const replaced = found !== null && (!usableFound || !sameMajor);
474
+ return { available: true, version, usable: true, compatible, persistent: false, ...(replaced ? { found } : {}) };
475
+ },
476
+ // Only a Pi older than the minimum reports its owner (pnpm or npm): the one the installer updates.
477
+ async pi() {
478
+ const pi = await globalPackage(PI_PACKAGE, "pi");
479
+ const older = (version) => !MAIN_BUILD.test(version) && !atLeast(version, requirements.pi);
480
+ if (pi.state === "present") return { available: true, version: pi.version, usable: true, ...(older(pi.version) ? { owner: "pnpm" } : {}) };
481
+ if (pi.state === "absent" || !pi.outsidePnpm) return pi.state === "absent" ? absent() : unknown();
482
+ // Another installation of Pi: reused as long as it reports a stable version.
483
+ const command = await lookPath("pi", user, platform, fs);
484
+ const invoked = command ? await invocation(command) : null;
485
+ const version = invoked ? /(?:^|\s|v)(\d+\.\d+\.\d+)(?:\s|$)/.exec(await output(invoked.command, [...invoked.prefix, "--version"], user, deadlines.version) ?? "") : null;
486
+ if (!version) return notPnpmGlobal(pi);
487
+ const found = { available: true, version: version[1], usable: true, external: true };
488
+ if (!older(found.version)) return found;
489
+ // npm owns it only when its package in npm's global root reports that same version.
490
+ const located = await piOnPath().catch(() => null);
491
+ return located?.owner === "npm" && located.version === found.version ? { ...found, owner: "npm" } : found;
492
+ },
493
+ async shell() {
494
+ const shell = await globalPackage(SHELL_PACKAGE, "gentle-shell");
495
+ if (shell.state === "present") return { available: true, version: shell.version, usable: await fs.isFile(shellBin()), global: true, owner: "pnpm" };
496
+ if (shell.state === "absent" || !shell.outsidePnpm) return shell.state === "absent" ? absent() : unknown();
497
+ // Installed by npm only when it really lives in npm's global root; a linked checkout stays unknown.
498
+ // A Windows shim is followed to the entry it runs, as for Pi.
499
+ const found = await packageOnPath("gentle-shell", SHELL_PACKAGE, true);
500
+ const owner = found ? installOwner({ packageRoot: found.root, pnpmHome: null, npmRoot: await npmGlobalRoot(), platform }) : null;
501
+ const version = found && (exactVersion(found.version, STABLE) ?? (MAIN_BUILD.test(found.version) ? found.version : null));
502
+ return owner === "npm" && version ? { available: true, version, usable: true, global: true, owner } : notPnpmGlobal(shell);
503
+ },
504
+ async gentleAi() {
505
+ const shell = await globalPackage(SHELL_PACKAGE, "gentle-shell");
506
+ if (shell.state === "absent") return absent();
507
+ // Only this package version's native pin is the requirement being checked.
508
+ if (shell.state !== "present" || shell.version !== requirements.shell) return unknown();
509
+ if (typeof shell.entry.path !== "string" || !path.isAbsolute(shell.entry.path)) return unknown();
510
+ const [root, pnpmHome] = [await fs.realpath(shell.entry.path), await fs.realpath(globalBin.pnpmHome)];
511
+ if (!contains(pnpmHome, root, platform)) return unknown();
512
+ if (!(await fs.isFile(gentleAiBinaryPath(root, platform)))) return absent();
513
+ const verdict = await verifyGentleAi({ packageRoot: root, platform, env, home: userHome });
514
+ return verdict?.ok === true ? { available: true, version: requirements.gentleAi, usable: true, compatible: true } : unknown();
515
+ },
516
+ async go() {
517
+ const go = await lookPath("go", user, platform, fs);
518
+ if (!go) return absent();
519
+ if (!spawnable(go, platform)) return unknown();
520
+ const version = exactVersion(await output(go, ["version"], user, deadlines.version), GO_VERSION);
521
+ return version ? { available: true, version, usable: true } : unknown();
522
+ },
523
+ // Not needed to install; review, worktrees and repository features use it.
524
+ // Git itself is never run: on macOS /usr/bin/git is a shim that opens an
525
+ // install dialog without the Command Line Tools, so only `xcode-select -p`
526
+ // (which never opens one) decides whether that shim is real Git.
527
+ async git() {
528
+ const git = await lookPath("git", user, platform, fs);
529
+ if (!git) return absent();
530
+ if (platform !== "darwin" || git !== "/usr/bin/git") return { available: true };
531
+ return { available: succeeded(await run("/usr/bin/xcode-select", ["-p"], { env: user, deadlineMs: deadlines.version })) };
532
+ },
533
+ async globalBin() {
534
+ if (!globalBin) return unknown();
535
+ // Nearest existing ancestor (or the directory itself); nothing is created.
536
+ let current = globalBin.path;
537
+ while (!(await fs.exists(current))) {
538
+ const parent = path.dirname(current);
539
+ if (parent === current) return unknown();
540
+ current = parent;
541
+ }
542
+ const writable = (await fs.isDirectory(current)) && (await fs.writable(current));
543
+ return { available: true, path: globalBin.path, writable, onPath: globalBin.onPath };
544
+ },
545
+ async setup() {
546
+ // Setup readiness of an existing Shell has no read-only evidence. Only the
547
+ // pinned stack this pnpm installed (an earlier run whose setup did not
548
+ // finish) is recoverable: its public `gentle-shell setup` may be rerun.
549
+ const shell = await globalPackage(SHELL_PACKAGE, "gentle-shell");
550
+ if (shell.state === "absent") return false;
551
+ if (shell.state !== "present") return unknown();
552
+ const root = await recoverableStackRoot(await globalListing(), globalBin.pnpmHome, platform, fs);
553
+ return root === null ? unknown() : { available: true, recoverable: true };
554
+ },
555
+ };
556
+ return Object.fromEntries(Object.entries(probes).map(([name, probe]) => [name, async () => {
557
+ try {
558
+ return await probe();
559
+ } catch {
560
+ // Do not retain probe errors: they can contain private paths or credentials.
561
+ return unknown();
562
+ }
563
+ }]));
564
+ }